UNPKG

@momentum-ui/react-collaboration

Version:

Cisco Momentum UI Framework for React Collaboration Applications

328 lines (286 loc) 9.74 kB
/** @component button-group */ import React from 'react'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; import qsa from 'dom-helpers/query/querySelectorAll'; import { UIDReset } from 'react-uid'; import ButtonGroupContext from '../ButtonGroupContext'; import SelectableContext from '../SelectableContext'; /** * @deprecated - Components in the legacy folder (/src/legacy) are deprecated. Please use a component from the components folder (/src/components) instead. Legacy components may not follow accessibility standards. **/ class ButtonGroup extends React.Component { static getDerivedStateFromProps({ active }, state) { return active ? { ...state, bgContext: { ...state.bgContext, active, }, } : state; } constructor(props) { super(props); this.state = { bgContext: { active: props.type === 'pill' ? false : props.highlightSelected && props.active, focus: props.active || null, isButtonGroup: true, ...(props.pillWidth && { width: props.pillWidth }), }, selectContext: { parentOnSelect: this.handleSelect, parentKeyDown: this.handleKeyDown, }, }; } componentDidMount() { this.containerNode && this.determineInitialFocus(); } componentDidUpdate(prevProps, prevState) { const { bgContext } = this.state; const { active } = this.props; if (prevProps.active !== active) { this.setActiveAndFocus(active, focus); } if (!this._needsRefocus || !this.containerNode) return; if (bgContext.focus && prevState.bgContext.focus !== bgContext.focus) { this.containerNode.querySelector(`[data-md-event-key="${bgContext.focus}"]`).focus(); } } determineInitialFocus = () => { const { bgContext } = this.state; const { focusFirstQuery, focusOnLoad } = this.props; const items = qsa( this.containerNode, focusFirstQuery || `.md-button:not(.disabled):not(:disabled)` ); let focus = bgContext.focus; if (items.length) { if (!focus) { focus = this.getNextFocusedChild(items, items[0], 0); } if (focus && focusOnLoad) { this.containerNode.querySelector(`[data-md-event-key="${focus}"]`).focus(); } } }; handleSelect = (e, opts) => { const { highlightSelected, onSelect, type } = this.props; const { active } = this.state.bgContext; const { eventKey } = opts; const items = this.getFocusableItems(); const index = items.indexOf( this.containerNode.querySelector(`[data-md-event-key="${eventKey}"]`) ); this.setFocus(items, index); // Don't do anything if onSelect Event Handler is present if (onSelect) { return onSelect(e, { eventKey: this.getValue(items, index, 'event'), }); } // Don't do anything if index is the same or outside of the bounds if (eventKey === active || index < 0 || index > items.length - 1) return; // Call change event handler this.setState((state) => ({ bgContext: { ...state.bgContext, active: type === 'pill' ? false : highlightSelected && this.getValue(items, index, 'event'), }, })); }; getNextFocusedChild(items, current, offset) { if (!this.containerNode) return null; const { bgContext } = this.state; const possibleIndex = items.indexOf(current) + offset; const getIndex = () => { if (possibleIndex < 0) { return items.length - 1; } else if (possibleIndex > items.length - 1) { return 0; } else return possibleIndex; }; bgContext.focus !== this.getValue(items, getIndex(), 'event') && this.setState({ bgContext: { ...bgContext, focus: this.getValue(items, getIndex(), 'event'), }, }); return this.getValue(items, getIndex(), 'event'); } getValue = (arr, index, attribute) => arr[index].attributes[`data-md-${attribute}-key`] && arr[index].attributes[`data-md-${attribute}-key`].value; getIncludesFirstCharacter = (str, char) => str.charAt(0).toLowerCase().includes(char); setFocus = (items, index) => { this.setState((state) => ({ bgContext: { ...state.bgContext, focus: this.getValue(items, index, 'event'), }, })); }; setActiveAndFocus = (active, focus) => { const { type, highlightSelected } = this.props; this._needsRefocus = false; this.setState((state) => ({ bgContext: { ...state.bgContext, active: type === 'pill' ? false : highlightSelected && active, focus: active || focus, }, })); }; setFocusByFirstCharacter = (char, focusIdx, items, length) => { const { bgContext } = this.state; const newIndex = items.reduce((agg, item, idx, arr) => { const index = focusIdx + idx + 1 > length ? Math.abs(focusIdx + idx - length) : focusIdx + idx + 1; return !agg.length && this.getValue(arr, index, 'keyboard') && this.getIncludesFirstCharacter(this.getValue(arr, index, 'keyboard'), char) ? agg.concat(this.getValue(arr, index, 'event')) : agg; }, []); typeof newIndex[0] === 'string' && bgContext.focus !== newIndex[0] && this.setState((state) => ({ bgContext: { ...state.bgContext, focus: newIndex[0], }, })); }; getFocusableItems = () => { if (!this.containerNode) return null; const { focusQuery } = this.props; const defaultItems = qsa(this.containerNode, `.md-button:not(.disabled):not(:disabled)`); const customItems = (focusQuery && qsa(this.containerNode, focusQuery)) || []; return customItems.length ? customItems.filter((item) => customItems.indexOf(item) >= 0) : defaultItems; }; handleKeyDown = (e) => { const { focus } = this.state.bgContext; let flag = false; const tgt = e.currentTarget; const char = e.key; const items = this.getFocusableItems(); const focusIdx = (focus && items.indexOf(this.containerNode.querySelector(`[data-md-event-key="${focus}"]`))) || 0; const length = (items.length && items.length - 1) || 0; const isPrintableCharacter = (str) => { return str.length === 1 && str.match(/\S/); }; switch (e.which) { case 38: case 37: this.getNextFocusedChild(items, tgt, -1); this._needsRefocus = true; flag = true; break; case 39: case 40: this.getNextFocusedChild(items, tgt, 1); this._needsRefocus = true; flag = true; break; default: if (isPrintableCharacter(char)) { this.setFocusByFirstCharacter(char, focusIdx, items, length); this._needsRefocus = true; flag = true; } break; } if (flag) { e.stopPropagation(); e.preventDefault(); } }; render() { const { ariaLabel, children, className, justified, theme, type, ...props } = this.props; const { bgContext, selectContext } = this.state; const otherProps = omit({ ...props }, [ 'active', 'focusOnLoad', 'focusFirstQuery', 'focusQuery', 'highlightSelected', 'onSelect', 'pillWidth', ]); return ( <SelectableContext.Provider value={selectContext}> <div aria-label={ariaLabel} className={ 'md-button-group' + `${(theme && ` md-button-group--${theme}`) || ''}` + `${(justified && ` md-button-group--justified`) || ''}` + `${(type && ` md-button-group--${type}`) || ''}` + `${(className && ` ${className}`) || ''}` } role="group" ref={(ref) => (this.containerNode = ref)} {...otherProps} > <UIDReset> <ButtonGroupContext.Provider value={bgContext}>{children}</ButtonGroupContext.Provider> </UIDReset> </div> </SelectableContext.Provider> ); } } ButtonGroup.propTypes = { /** @prop Sets initial active Button by index | null */ active: PropTypes.string, /** @prop Text to display for blindness accessibility features | '' */ ariaLabel: PropTypes.string, /** @prop Children nodes to render inside ButtonGroup | null */ children: PropTypes.node, /** @prop Optional css class string | '' */ className: PropTypes.string, /** @prop Set focus to ButtonGroup when page is loaded | false */ focusOnLoad: PropTypes.bool, /** @prop Queries children to find matching item to have focus | '' */ focusFirstQuery: PropTypes.string, /** @prop Additional elements that can be focused by selector | '' */ focusQuery: PropTypes.string, /** @prop Highlights the selected button within group | true */ highlightSelected: PropTypes.bool, /** @prop Optional text-justified css styling | true */ justified: PropTypes.bool, /** @prop Handler to be called when the user selects ButtonGroup | null */ onSelect: PropTypes.func, /** @prop Sets width of a pill Button | '60px' */ pillWidth: PropTypes.string, /** @prop Optional Button color theme for ButtonGroup | '' */ theme: PropTypes.oneOf(['', 'dark']), /** @prop Optional Button type for ButtonGroup | '' */ type: PropTypes.oneOf(['', 'pill', 'unstyled']), }; ButtonGroup.defaultProps = { active: '', ariaLabel: '', children: null, className: '', focusOnLoad: false, focusFirstQuery: '', focusQuery: '', highlightSelected: true, justified: true, onSelect: null, pillWidth: '60px', theme: '', type: '', }; ButtonGroup.displayName = 'ButtonGroup'; export default ButtonGroup;