diff --git a/react/src/lib/ActivityButton/tests/__snapshots__/index.spec.js.snap b/react/src/lib/ActivityButton/tests/__snapshots__/index.spec.js.snap
index 20b9299333..49cd2c2b92 100644
--- a/react/src/lib/ActivityButton/tests/__snapshots__/index.spec.js.snap
+++ b/react/src/lib/ActivityButton/tests/__snapshots__/index.spec.js.snap
@@ -23,11 +23,9 @@ ShallowWrapper {
Symbol(enzyme.__node__): Object {
"instance": null,
"key": undefined,
- "nodeType": "class",
+ "nodeType": "function",
"props": Object {
- "active": false,
"ariaLabel": "test",
- "ariaLabelledBy": "",
"children": ,
"circle": true,
"className": "md-activity md-activity__chat",
- "color": "",
+ "color": undefined,
"containerLarge": false,
"disabled": false,
- "expand": false,
- "href": "",
- "index": null,
- "isButtonGroup": false,
- "label": "",
- "large": false,
- "loading": false,
"onClick": null,
- "removeStyle": false,
"size": 68,
- "style": Object {},
- "tag": "button",
- "type": "button",
},
"ref": null,
"rendered": Object {
@@ -90,17 +77,20 @@ ShallowWrapper {
"rendered": null,
"type": [Function],
},
- "type": [Function],
+ "type": Object {
+ "$$typeof": Symbol(react.forward_ref),
+ "defaultProps": undefined,
+ "propTypes": undefined,
+ "render": [Function],
+ },
},
Symbol(enzyme.__nodes__): Array [
Object {
"instance": null,
"key": undefined,
- "nodeType": "class",
+ "nodeType": "function",
"props": Object {
- "active": false,
"ariaLabel": "test",
- "ariaLabelledBy": "",
"children": ,
"circle": true,
"className": "md-activity md-activity__chat",
- "color": "",
+ "color": undefined,
"containerLarge": false,
"disabled": false,
- "expand": false,
- "href": "",
- "index": null,
- "isButtonGroup": false,
- "label": "",
- "large": false,
- "loading": false,
"onClick": null,
- "removeStyle": false,
"size": 68,
- "style": Object {},
- "tag": "button",
- "type": "button",
},
"ref": null,
"rendered": Object {
@@ -163,7 +142,12 @@ ShallowWrapper {
"rendered": null,
"type": [Function],
},
- "type": [Function],
+ "type": Object {
+ "$$typeof": Symbol(react.forward_ref),
+ "defaultProps": undefined,
+ "propTypes": undefined,
+ "render": [Function],
+ },
},
],
Symbol(enzyme.__options__): Object {
diff --git a/react/src/lib/ActivityButton/tests/index.spec.js b/react/src/lib/ActivityButton/tests/index.spec.js
index 0cbebec857..f37951d94c 100644
--- a/react/src/lib/ActivityButton/tests/index.spec.js
+++ b/react/src/lib/ActivityButton/tests/index.spec.js
@@ -10,7 +10,7 @@ describe('tests for ', () => {
});
it('should render one ActivityButton button', () => {
- const container = shallow();
+ const container = mount();
expect(container.find('Button').length).toEqual(1);
});
diff --git a/react/src/lib/Alert/tests/__snapshots__/index.spec.js.snap b/react/src/lib/Alert/tests/__snapshots__/index.spec.js.snap
index 814009ee60..670927ae7d 100644
--- a/react/src/lib/Alert/tests/__snapshots__/index.spec.js.snap
+++ b/react/src/lib/Alert/tests/__snapshots__/index.spec.js.snap
@@ -46,28 +46,9 @@ ShallowWrapper {
@@ -149,32 +131,70 @@ class Button extends React.Component {
['none', '28', '36', '40', '52', 28, 36, 40, 52].includes(size)
);
- const button = React.createElement(
+ const getTabIndex = ({ isButtonGroup, focus }) => {
+ if(!isButtonGroup) {
+ return 0;
+ } else if (isButtonGroup && focus) {
+ return 0;
+ } else return -1;
+ };
+
+ const withContext = () => (
+
`md-button-${id}`}>
+ {id => (
+
+ {context => {
+ let contextProps = {};
+
+ contextProps.id = this.props.id || id;
+ contextProps.uniqueKey = eventKey || contextProps.id;
+ contextProps.active = active || (context && context.active === contextProps.uniqueKey);
+ contextProps.isButtonGroup = (context && context.isButtonGroup) || false;
+ contextProps.focus = context && context.focus === contextProps.uniqueKey;
+ contextProps.width = (context && context.width) || null;
+ contextProps.tabIndex = disabled ? null : getTabIndex(contextProps);
+
+ return createElement(contextProps);
+ }}
+
+ )}
+
+ );
+
+ const keyboardNavKey = makeKeyboardKey(keyboardKey || label || children);
+
+ const createElement = cxtProps => React.createElement(
tag,
{
- ref: 'button',
+ ref: ref => this.button = ref,
className:
'md-button' +
`${(circle && ` md-button--circle`) || ''}` +
- `${(isButtonGroupIcon() && ` md-button--icon-group`) || ''}` +
+ `${(isButtonGroupIcon(cxtProps.isButtonGroup) && ` md-button--icon-group`) || ''}` +
`${(getSize() && ` md-button--${getSize()}`) || ''}` +
`${(expand && ` md-button--expand`) || ''}` +
`${(color && ` md-button--${getColor()}`) || ''}` +
`${(removeStyle && ' md-button--none') || ''}` +
- `${(active && !disabled && ` active`) || ''}` +
+ `${(cxtProps.active && !disabled && ` active`) || ''}` +
`${(className && ` ${className}`) || ''}`,
- onClick: this.handleClick,
- onKeyDown: this.handleKeyDown,
- style: style,
+ id: cxtProps.id,
+ 'data-md-event-key': cxtProps.uniqueKey,
+ onClick: e => this.handleClick(e, cxtProps.uniqueKey),
+ onKeyDown: e => this.handleKeyDown(e, cxtProps.uniqueKey),
+ style: {
+ style,
+ ...cxtProps.width && { width: cxtProps.width }
+ },
disabled: disabled || loading,
alt: ariaLabel || label,
href: (tag === 'a' && href) || undefined,
type: tag !== 'a' && type || '',
+ ...keyboardNavKey && { 'data-md-keyboard-key': keyboardNavKey },
+ ...cxtProps.focus && { 'aria-current': `${cxtProps.focus}` },
...ariaLabel
? { 'aria-label': ariaLabel }
: { 'aria-labelledby': ariaLabelledBy },
- tabIndex: (typeof index !== 'number'
- || index === focusIndex) ? 0 : -1,
+ tabIndex: cxtProps.tabIndex,
...tag && tag !== 'button' && {role: 'button'},
...otherProps,
},
@@ -187,24 +207,17 @@ class Button extends React.Component {
- {button}
+ {withContext()}
{label}
:
- button
+ withContext()
);
}
}
-Button.contextTypes = {
- focusIndex: PropTypes.number,
- focusOnLoad: PropTypes.bool,
- handleClick: PropTypes.func,
- handleKeyDown: PropTypes.func,
-};
-
Button.propTypes = {
/** @prop Sets active css styling | false */
active: PropTypes.bool,
@@ -224,14 +237,16 @@ Button.propTypes = {
containerLarge: PropTypes.bool,
/** @prop Sets the attribute disabled to Button | false */
disabled: PropTypes.bool,
+ /** @prop Unique string used for tracking events among ancestors | '' */
+ eventKey: PropTypes.string,
/** @prop Sets expand css styling to widen the Button | false */
expand: PropTypes.bool,
/** @prop Href prop changes element to anchor element | '' */
href: PropTypes.string,
- /** @prop This index is used to control focus of Button within a ButtonGroup | null */
- index: PropTypes.number,
- /** @prop Determines whether class should be applied to ButtonGroups with Icons as descendants | false */
- isButtonGroup: PropTypes.bool,
+ /** @prop Sets Button id | null */
+ id: PropTypes.string,
+ /** @prop Unique string used for keyboard navigation | '' */
+ keyboardKey: PropTypes.string,
/** @prop Text to display inside the button | '' */
label: PropTypes.string,
/** @prop Depreciated large css styling, use size instead | false */
@@ -240,6 +255,10 @@ Button.propTypes = {
loading: PropTypes.bool,
/** @prop Handler to be called when the user taps the button | null */
onClick: PropTypes.func,
+ // Internal Context Use Only
+ parentKeyDown: PropTypes.func,
+ // Internal Context Use Only
+ parentOnSelect: PropTypes.func,
/** @prop Optional prop to remove Button's default style | false */
removeStyle: PropTypes.bool,
/** @prop Optional string or number size prop | 36 */
@@ -262,14 +281,17 @@ Button.defaultProps = {
color: '',
containerLarge: false,
disabled: false,
+ eventKey: '',
expand: false,
href: '',
- index: null,
- isButtonGroup: false,
+ id: '',
+ keyboardKey: '',
label: '',
large: false,
loading: false,
onClick: null,
+ parentKeyDown: null,
+ parentOnSelect: null,
removeStyle: false,
size: 36,
style: {},
@@ -277,4 +299,10 @@ Button.defaultProps = {
type: 'button',
};
-export default Button;
+Button.displayName = 'Button';
+
+export default mapContextToProps(
+ SelectableContext,
+ context => context,
+ Button
+);
diff --git a/react/src/lib/Button/tests/__snapshots__/index.spec.js.snap b/react/src/lib/Button/tests/__snapshots__/index.spec.js.snap
index 6868eab36a..7c950dc9cd 100644
--- a/react/src/lib/Button/tests/__snapshots__/index.spec.js.snap
+++ b/react/src/lib/Button/tests/__snapshots__/index.spec.js.snap
@@ -3,31 +3,11 @@
exports[`tests for
should match SnapShot 1`] = `
ShallowWrapper {
Symbol(enzyme.__root__): [Circular],
- Symbol(enzyme.__unrendered__):
,
+ ,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
"checkPropTypes": [Function],
@@ -40,103 +20,59 @@ ShallowWrapper {
Symbol(enzyme.__node__): Object {
"instance": null,
"key": undefined,
- "nodeType": "host",
+ "nodeType": "function",
"props": Object {
- "alt": "test",
- "aria-label": "test",
- "children": Array [
- false,
-
- test
- ,
- ],
- "className": "md-button md-button--36",
- "disabled": false,
- "href": undefined,
- "onClick": [Function],
- "onKeyDown": [Function],
- "style": Object {},
- "tabIndex": 0,
- "type": "button",
+ "children": [Function],
},
- "ref": "button",
- "rendered": Array [
- false,
- Object {
- "instance": null,
- "key": "child-1",
- "nodeType": "host",
- "props": Object {
- "children": "test",
- "className": "md-button__children",
- "style": Object {
- "opacity": "1",
- },
+ "ref": null,
+ "rendered": [Function],
+ "type": Object {
+ "$$typeof": Symbol(react.context),
+ "_calculateChangedBits": null,
+ "_context": Object {
+ "$$typeof": Symbol(react.context),
+ "Consumer": [Circular],
+ "Provider": Object {
+ "$$typeof": Symbol(react.provider),
+ "_context": [Circular],
},
- "ref": null,
- "rendered": "test",
- "type": "span",
+ "_calculateChangedBits": null,
+ "_currentRenderer": null,
+ "_currentRenderer2": null,
+ "_currentValue": undefined,
+ "_currentValue2": undefined,
+ "_threadCount": 0,
},
- ],
- "type": "button",
+ },
},
Symbol(enzyme.__nodes__): Array [
Object {
"instance": null,
"key": undefined,
- "nodeType": "host",
+ "nodeType": "function",
"props": Object {
- "alt": "test",
- "aria-label": "test",
- "children": Array [
- false,
-
- test
- ,
- ],
- "className": "md-button md-button--36",
- "disabled": false,
- "href": undefined,
- "onClick": [Function],
- "onKeyDown": [Function],
- "style": Object {},
- "tabIndex": 0,
- "type": "button",
+ "children": [Function],
},
- "ref": "button",
- "rendered": Array [
- false,
- Object {
- "instance": null,
- "key": "child-1",
- "nodeType": "host",
- "props": Object {
- "children": "test",
- "className": "md-button__children",
- "style": Object {
- "opacity": "1",
- },
+ "ref": null,
+ "rendered": [Function],
+ "type": Object {
+ "$$typeof": Symbol(react.context),
+ "_calculateChangedBits": null,
+ "_context": Object {
+ "$$typeof": Symbol(react.context),
+ "Consumer": [Circular],
+ "Provider": Object {
+ "$$typeof": Symbol(react.provider),
+ "_context": [Circular],
},
- "ref": null,
- "rendered": "test",
- "type": "span",
+ "_calculateChangedBits": null,
+ "_currentRenderer": null,
+ "_currentRenderer2": null,
+ "_currentValue": undefined,
+ "_currentValue2": undefined,
+ "_threadCount": 0,
},
- ],
- "type": "button",
+ },
},
],
Symbol(enzyme.__options__): Object {
@@ -165,6 +101,5 @@ ShallowWrapper {
Symbol(enzyme.__providerValues__): undefined,
},
Symbol(enzyme.__providerValues__): Map {},
- Symbol(enzyme.__childContext__): null,
}
`;
diff --git a/react/src/lib/Button/tests/index.spec.js b/react/src/lib/Button/tests/index.spec.js
index deb45586da..177a570289 100644
--- a/react/src/lib/Button/tests/index.spec.js
+++ b/react/src/lib/Button/tests/index.spec.js
@@ -1,6 +1,8 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import { Button, Loading } from '@momentum-ui/react';
+import ButtonGroupContext from '../../ButtonGroupContext';
+import SelectableContext from '../../SelectableContext';
describe('tests for
', () => {
it('should match SnapShot', () => {
@@ -10,13 +12,13 @@ describe('tests for
', () => {
});
it('should render one Button', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.find('button').length).toEqual(1);
});
it('should render one Child Div', () => {
- const container = shallow(
+ const container = mount(
@@ -26,25 +28,25 @@ describe('tests for
', () => {
});
it('should render Loader Component if Loading', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.contains(
)).toEqual(true);
});
it('should not render Loader Component if loading prop absent', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.contains(
)).toEqual(false);
});
it('should remove Style with removeStyle prop', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.find('.md-button--none').exists()).toEqual(true);
});
it('should ignore color or size with removeStyle prop', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.find('.md-button--blue').exists()).toEqual(false);
expect(container.find('.md-button--52').exists()).toEqual(false);
@@ -57,49 +59,49 @@ describe('tests for
', () => {
});
it('should render wrapped button if label passed', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.find('.md-button__container--small').length).toEqual(1);
});
it('should render wrapped button in large container if label and containerLarge passed', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.find('.md-button__container').length).toEqual(1);
});
it('should be type button by default', () => {
- const container = shallow(
);
+ const container = mount(
);
- expect(container.props().type).toEqual('button');
+ expect(container.find('Button').props().type).toEqual('button');
});
it('should show active class when passed active prop', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.find('.md-button').hasClass('active')).toEqual(true);
});
it('should show type if passed one', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.props().type).toEqual('submit');
});
it('should output anchor if passed tag a', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.find('a').length).toEqual(1);
});
it('should output input if passed tag input', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.find('input').length).toEqual(1);
});
it('should handle disabled state', () => {
- const container = shallow(
);
+ const container = mount(
);
expect(container.props().disabled).toEqual(true);
});
@@ -107,11 +109,11 @@ describe('tests for
', () => {
it('should handle onClick event', () => {
const handleClick = jest.fn();
const onClick = jest.fn();
- const container = shallow(
, {
- context: {
- handleClick: handleClick
- }
- });
+ const container = mount(
+
+
+
+ );
container.find('button').simulate('click');
expect(onClick).toHaveBeenCalledTimes(1);
@@ -121,11 +123,11 @@ describe('tests for
', () => {
it('should handle keyDown as onClick event for enter/space key', () => {
const handleClick = jest.fn();
const onClick = jest.fn();
- const container = mount(
, {
- context: {
- handleClick: handleClick
- }
- });
+ const container = mount(
+
+
+
+ );
container
.find('button')
@@ -138,11 +140,12 @@ describe('tests for
', () => {
it('should call context handleKeyDown callback on keyDown event (other than enter/space)', () => {
const handleKeyDown = jest.fn();
- const container = mount(
, {
- context: {
- handleKeyDown: handleKeyDown
- }
- });
+ const container = mount(
+
+
+
+ );
+
container
.find('button')
.simulate('keyDown', { which: 39, charCode: 39, key: 'Right' });
@@ -151,20 +154,22 @@ describe('tests for
', () => {
describe('tabIndex value of the button', () => {
it('when the button is focused tabIndex should be zero', () => {
- const container = mount(
, {
- context: {
- focusIndex: 0
- }
- });
+ const container = mount(
+
+
+
+ );
+
expect(container.find('button').props().tabIndex).toEqual(0);
});
it('when the button is not focused tabIndex should be -1', () => {
- const container = mount(
, {
- context: {
- focusIndex: 1
- }
- });
+ const container = mount(
+
+
+
+ );
+
expect(container.find('button').props().tabIndex).toEqual(-1);
});
diff --git a/react/src/lib/ButtonGroup/index.js b/react/src/lib/ButtonGroup/index.js
index ecbb8fb2a3..6c694f656b 100644
--- a/react/src/lib/ButtonGroup/index.js
+++ b/react/src/lib/ButtonGroup/index.js
@@ -2,94 +2,139 @@
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';
class ButtonGroup extends React.Component {
- static displayName = 'ButtonGroup';
-
- static childContextTypes = {
- handleClick: PropTypes.func,
- handleKeyDown: PropTypes.func,
- focusIndex: PropTypes.number,
- focusOnLoad: PropTypes.bool,
- };
+ static getDerivedStateFromProps({ active }, state) {
+ return (
+ active
+ ? {
+ ...state,
+ bgContext: {
+ ...state.bgContext,
+ active
+ }
+ }
+ : state
+ );
+ }
- state = {
- activeIndex: this.props.activeIndex,
- focusIndex: 0,
- };
+ constructor(props) {
+ super(props);
- getChildContext = () => {
- return {
- handleClick: (event, index) => this.handleClick(event, index),
- handleKeyDown: (event, index) => this.handleKeyDown(event, index),
- focusIndex: this.state.focusIndex,
- focusOnLoad: this.props.focusOnLoad,
+ 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() {
- const { focusIndex, activeIndex } = this.state;
- const initialFocus = this.getNewIndex(focusIndex - 1 , 1);
- this.setFocusIndex(initialFocus);
- (activeIndex !== null) && this.determineInitialActive();
+ this.containerNode
+ && this.determineInitialFocus();
}
- determineInitialActive = () => {
- /* eslint-disable no-console */
- const { activeIndex, children } = this.state;
- if(activeIndex < 0 && activeIndex > children.length - 1) {
- console.warn('[@momentum-ui/react] ButtonGroup: activeIndex is out of bound');
- return;
+ componentDidUpdate(prevProps, prevState) {
+ const { bgContext } = this.state;
+ const { active } = this.props;
+ if ((prevProps.active !== active)) {
+ this.setActiveAndFocus(active, focus);
}
- const initialActive = this.getNewIndex(activeIndex - 1 , 1);
- this.setActiveIndex(initialActive);
- /* eslint-enable no-console */
- };
+ if (!this._needsRefocus || !this.containerNode) return;
- setFocusIndex = index => {
- const { focusIndex } = this.state;
- return (
- focusIndex !== index
- && this.setState({ focusIndex: index })
- );
- };
+ if (bgContext.focus && prevState.bgContext.focus !== bgContext.focus) {
+ this.containerNode.querySelector(`[data-md-event-key="${bgContext.focus}"]`).focus();
+ }
+ }
- setActiveIndex = index => {
- const { activeIndex } = this.state;
- return (
- activeIndex !== index
- && this.setState({ activeIndex: index })
- );
- };
+ determineInitialFocus = () => {
+ const { bgContext } = this.state;
+ const { focusFirstQuery, focusOnLoad } = this.props;
+ const items = qsa(this.containerNode, focusFirstQuery || `.md-button:not(.disabled):not(:disabled)`);
- handleClick = (event, index) => {
- const { onSelect } = this.props;
- this.setFocusIndex(index);
- this.setActiveIndex(index);
- onSelect && onSelect(event, index);
- };
+ 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();
+ }
+ }
+ }
- getNewIndex = (currentIndex, change) => {
- const { children } = this.props;
- const length = children.length - 1;
+ handleSelect = (e, opts) => {
+ const { highlightSelected, onSelect, type } = this.props;
+ const { active } = this.state.bgContext;
+ const { eventKey } = opts;
- const getPossibleIndex = () => {
- if (currentIndex + change < 0) {
- return length;
- } else if (currentIndex + change > length) {
- return 0;
+ 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;
- return currentIndex + change;
+ 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;
};
- const possibleIndex = getPossibleIndex();
- const potentialTarget = React.Children.toArray(children)[possibleIndex];
+ bgContext.focus !== this.getValue(items, getIndex(), 'event')
+ && this.setState({
+ bgContext: {
+ ...bgContext,
+ focus: this.getValue(items, getIndex(), 'event'),
+ }
+ });
- return potentialTarget.props.disabled
- ? this.getNewIndex(possibleIndex, change)
- : possibleIndex;
- };
+ 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
@@ -97,41 +142,79 @@ class ButtonGroup extends React.Component {
.toLowerCase()
.includes(char);
- setFocusByFirstCharacter = (char, currentIdx) => {
- const { children } = this.props;
- const length = children.length - 1;
+ setFocus = (items, index) => {
+ this.setState(state => ({
+ bgContext: {
+ ...state.bgContext,
+ focus: this.getValue(items, index, 'event'),
+ }
+ }));
+ }
- const newIndex = React.Children
- .toArray(children)
- .reduce((agg, child, idx, arr) => {
+ 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 index = currentIdx + idx + 1 > length
- ? Math.abs(currentIdx + idx - length)
- : currentIdx + idx + 1;
+ const newIndex = items
+ .reduce((agg, item, idx, arr) => {
- const label = typeof arr[index].props.children === 'string'
- ? arr[index].props.children
- : '';
+ const index = focusIdx + idx + 1 > length
+ ? Math.abs(focusIdx + idx - length)
+ : focusIdx + idx + 1;
return (
!agg.length
- && !arr[index].props.disabled
- && !arr[index].props.isReadOnly
- && this.getIncludesFirstCharacter(label, char)
+ && this.getValue(arr, index, 'keyboard')
+ && this.getIncludesFirstCharacter(this.getValue(arr, index, 'keyboard'), char)
)
- ? agg.concat(index)
+ ? agg.concat(this.getValue(arr, index, 'event'))
: agg;
},
[]
);
- !isNaN(newIndex[0]) && this.setFocusIndex(newIndex[0]);
- };
- handleKeyDown = (e, idx) => {
+ 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;
- let newIndex;
+ 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/);
@@ -140,20 +223,21 @@ class ButtonGroup extends React.Component {
switch (e.which) {
case 38:
case 37:
- newIndex = this.getNewIndex(idx, -1);
- this.setFocusIndex(newIndex);
+ this.getNextFocusedChild(items, tgt, -1);
+ this._needsRefocus = true;
flag = true;
break;
case 39:
case 40:
- newIndex = this.getNewIndex(idx, 1);
- this.setFocusIndex(newIndex);
+ this.getNextFocusedChild(items, tgt, 1);
+ this._needsRefocus = true;
flag = true;
break;
default:
if (isPrintableCharacter(char)) {
- this.setFocusByFirstCharacter(char, idx);
+ this.setFocusByFirstCharacter(char, focusIdx, items, length);
+ this._needsRefocus = true;
flag = true;
}
break;
@@ -170,49 +254,52 @@ class ButtonGroup extends React.Component {
ariaLabel,
children,
className,
- highlightSelected,
justified,
- pillWidth,
theme,
type,
+ ...props
} = this.props;
- const { activeIndex } = this.state;
-
- const setButtons = () =>
- React.Children.map(children, (child, idx) => (
- child
- ? React.cloneElement(child, {
- active: type === 'pill' ? false : highlightSelected && activeIndex === idx,
- index: idx,
- isButtonGroup: true,
- style: {
- ...pillWidth && {width: pillWidth},
- }
- })
- : child
- ));
+ const { bgContext, selectContext } = this.state;
+
+ const otherProps = omit({...props}, [
+ 'active',
+ 'focusOnLoad',
+ 'focusFirstQuery',
+ 'focusQuery',
+ 'highlightSelected',
+ 'onSelect',
+ 'pillWidth',
+ ]);
return (
-
- {setButtons()}
-
+
+ this.containerNode = ref}
+ {...otherProps}
+ >
+
+
+ {children}
+
+
+
+
);
}
}
ButtonGroup.propTypes = {
/** @prop Sets initial active Button by index | null */
- activeIndex: PropTypes.number,
+ active: PropTypes.string,
/** @prop Text to display for blindness accessibility features | '' */
ariaLabel: PropTypes.string,
/** @prop Children nodes to render inside ButtonGroup | null */
@@ -221,6 +308,10 @@ ButtonGroup.propTypes = {
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 */
@@ -236,11 +327,13 @@ ButtonGroup.propTypes = {
};
ButtonGroup.defaultProps = {
- activeIndex: null,
+ active: '',
ariaLabel: '',
children: null,
className: '',
focusOnLoad: false,
+ focusFirstQuery: '',
+ focusQuery: '',
highlightSelected: true,
justified: true,
onSelect: null,
@@ -249,4 +342,6 @@ ButtonGroup.defaultProps = {
type:'',
};
+ButtonGroup.displayName = 'ButtonGroup';
+
export default ButtonGroup;
diff --git a/react/src/lib/ButtonGroup/tests/__snapshots__/index.spec.js.snap b/react/src/lib/ButtonGroup/tests/__snapshots__/index.spec.js.snap
index 8b7cb90d9c..c02bd113e4 100644
--- a/react/src/lib/ButtonGroup/tests/__snapshots__/index.spec.js.snap
+++ b/react/src/lib/ButtonGroup/tests/__snapshots__/index.spec.js.snap
@@ -4,10 +4,12 @@ exports[`tests for
should match SnapShot 1`] = `
ShallowWrapper {
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__):
-
+
,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
@@ -53,162 +35,330 @@ ShallowWrapper {
Symbol(enzyme.__node__): Object {
"instance": null,
"key": undefined,
- "nodeType": "host",
+ "nodeType": "function",
"props": Object {
- "aria-label": "",
- "children": Array [
-
,
- ],
- "className": "md-button-group md-button-group--justified",
- "role": "group",
+ >
+
+ 1
+
+
+
+
,
+ "value": Object {
+ "parentKeyDown": [Function],
+ "parentOnSelect": [Function],
+ },
},
"ref": null,
- "rendered": Array [
- Object {
- "instance": null,
- "key": ".0",
- "nodeType": "class",
- "props": Object {
- "active": false,
- "ariaLabel": "test",
- "ariaLabelledBy": "",
- "children": "1",
- "circle": false,
- "className": "",
- "color": "",
- "containerLarge": false,
- "disabled": false,
- "expand": false,
- "href": "",
- "index": 0,
- "isButtonGroup": true,
- "label": "",
- "large": false,
- "loading": false,
- "onClick": null,
- "removeStyle": false,
- "size": 36,
- "style": Object {
- "width": "60px",
- },
- "tag": "button",
- "type": "button",
- },
- "ref": null,
- "rendered": "1",
- "type": [Function],
- },
- ],
- "type": "div",
- },
- Symbol(enzyme.__nodes__): Array [
- Object {
+ "rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "host",
"props": Object {
"aria-label": "",
- "children": Array [
-