diff --git a/packages/react/src/components/Tab/Tab-test.js b/packages/react/src/components/Tab/Tab-test.js index fdd59ac083c4..b7d526f8b9c1 100644 --- a/packages/react/src/components/Tab/Tab-test.js +++ b/packages/react/src/components/Tab/Tab-test.js @@ -101,21 +101,18 @@ describe('Tab', () => { describe('keydown', () => { const onKeyDown = jest.fn(); - const handleTabAnchorFocus = jest.fn(); const handleTabKeyDown = jest.fn(); const wrapper = shallow(); - wrapper.setProps({ onKeyDown, handleTabAnchorFocus, handleTabKeyDown }); + wrapper.setProps({ onKeyDown, handleTabKeyDown }); it('invokes onKeyDown when a function is passed to onKeyDown prop', () => { wrapper.simulate('keyDown', { which: 38 }); expect(onKeyDown).toBeCalled(); - expect(handleTabAnchorFocus).not.toBeCalled(); }); it('invokes handleTabAnchorFocus when onKeyDown occurs for appropriate events', () => { wrapper.simulate('keyDown', { which: 37 }); expect(onKeyDown).toBeCalled(); - expect(handleTabAnchorFocus).toBeCalled(); }); }); }); diff --git a/packages/react/src/components/Tab/Tab.js b/packages/react/src/components/Tab/Tab.js index 5c35b034cfff..dde7ce38914a 100644 --- a/packages/react/src/components/Tab/Tab.js +++ b/packages/react/src/components/Tab/Tab.js @@ -26,12 +26,6 @@ export default class Tab extends React.Component { */ handleTabClick: PropTypes.func, - /** - * A handler that is invoked when a user presses left/right key. - * Reserved for usage in Tabs - */ - handleTabAnchorFocus: PropTypes.func, - /** * A handler that is invoked on the key down event for the control. * Reserved for usage in Tabs @@ -108,23 +102,10 @@ export default class Tab extends React.Component { onKeyDown: () => {}, }; - setTabFocus(evt) { - const leftKey = 37; - const rightKey = 39; - if (evt.which === leftKey) { - this.props.handleTabAnchorFocus(this.props.index - 1); - } else if (evt.which === rightKey) { - this.props.handleTabAnchorFocus(this.props.index + 1); - } else { - return; - } - } - render() { const { className, handleTabClick, - handleTabAnchorFocus, // eslint-disable-line handleTabKeyDown, disabled, href, @@ -172,7 +153,6 @@ export default class Tab extends React.Component { if (disabled) { return; } - this.setTabFocus(evt); handleTabKeyDown(index, evt); onKeyDown(evt); }} diff --git a/packages/react/src/components/Tabs/Tabs-story.js b/packages/react/src/components/Tabs/Tabs-story.js index 1adea0976a6c..e49bdfbc7fbf 100644 --- a/packages/react/src/components/Tabs/Tabs-story.js +++ b/packages/react/src/components/Tabs/Tabs-story.js @@ -78,14 +78,17 @@ storiesOf('Tabs', module)
Content for second tab goes here.
+ +
Content for third tab goes here.
+
-
Content for third tab goes here.
+
Content for fourth tab goes here.
}> -
Content for fourth tab goes here.
+
Content for fifth tab goes here.
), diff --git a/packages/react/src/components/Tabs/Tabs-test.js b/packages/react/src/components/Tabs/Tabs-test.js index 55f5034c7032..365ae339c2a7 100644 --- a/packages/react/src/components/Tabs/Tabs-test.js +++ b/packages/react/src/components/Tabs/Tabs-test.js @@ -7,14 +7,25 @@ import React from 'react'; import { ChevronDownGlyph } from '@carbon/icons-react'; +import { settings } from 'carbon-components'; +import { shallow, mount } from 'enzyme'; import Tabs from '../Tabs'; import Tab from '../Tab'; import TabsSkeleton from '../Tabs/Tabs.Skeleton'; -import { shallow, mount } from 'enzyme'; -import { settings } from 'carbon-components'; const { prefix } = settings; +window.matchMedia = jest.fn().mockImplementation(query => ({ + matches: true, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), +})); + describe('Tabs', () => { describe('renders as expected', () => { describe('navigation (
)', () => { @@ -241,6 +252,53 @@ describe('Tabs', () => { expect(wrapper.state().selected).toEqual(1); }); }); + + describe('ignore disabled child tab', () => { + let wrapper; + let firstTab; + let lastTab; + beforeEach(() => { + wrapper = mount( + + + content1 + + + content2 + + + content3 + + + ); + firstTab = wrapper.find('.firstTab').last(); + lastTab = wrapper.find('.lastTab').last(); + }); + it('updates selected state when pressing arrow keys', () => { + firstTab.simulate('keydown', { which: rightKey }); + expect(wrapper.state().selected).toEqual(2); + lastTab.simulate('keydown', { which: leftKey }); + expect(wrapper.state().selected).toEqual(0); + }); + + it('loops focus and selected state from lastTab to firstTab', () => { + wrapper.setState({ selected: 2 }); + lastTab.simulate('keydown', { which: rightKey }); + expect(wrapper.state().selected).toEqual(0); + }); + + it('loops focus and selected state from firstTab to lastTab', () => { + firstTab.simulate('keydown', { which: leftKey }); + expect(wrapper.state().selected).toEqual(2); + }); + + it('updates selected state when pressing space or enter key', () => { + firstTab.simulate('keydown', { which: spaceKey }); + expect(wrapper.state().selected).toEqual(0); + lastTab.simulate('keydown', { which: enterKey }); + expect(wrapper.state().selected).toEqual(2); + }); + }); }); }); diff --git a/packages/react/src/components/Tabs/Tabs.js b/packages/react/src/components/Tabs/Tabs.js index 00979a33b700..5b2372a8e5af 100644 --- a/packages/react/src/components/Tabs/Tabs.js +++ b/packages/react/src/components/Tabs/Tabs.js @@ -10,6 +10,7 @@ import React from 'react'; import classNames from 'classnames'; import { ChevronDownGlyph } from '@carbon/icons-react'; import { settings } from 'carbon-components'; +import { keys, match, matches } from '../../internal/keyboard'; const { prefix } = settings; @@ -116,6 +117,12 @@ export default class Tabs extends React.Component { return React.Children.map(this.props.children, tab => tab); } + getEnabledTabs = () => + React.Children.toArray(this.props.children).reduce( + (acc, tab, index) => (!tab.props.disabled ? acc.concat(index) : acc), + [] + ); + getTabAt = (index, useFresh) => { return ( (!useFresh && this[`tab${index}`]) || @@ -139,35 +146,47 @@ export default class Tabs extends React.Component { }; }; + getDirection = evt => { + if (match(evt, keys.ArrowLeft)) { + return -1; + } + if (match(evt, keys.ArrowRight)) { + return 1; + } + return 0; + }; + + getNextIndex = (index, direction) => { + const enabledTabs = this.getEnabledTabs(); + const nextIndex = Math.max( + enabledTabs.indexOf(index) + direction, + -1 /* For `tab` not found in `enabledTabs` */ + ); + const nextIndexLooped = + nextIndex >= 0 && nextIndex < enabledTabs.length + ? nextIndex + : nextIndex - Math.sign(nextIndex) * enabledTabs.length; + return enabledTabs[nextIndexLooped]; + }; + handleTabKeyDown = onSelectionChange => { return (index, evt) => { - const key = evt.key || evt.which; - - if (key === 'Enter' || key === 13 || key === ' ' || key === 32) { + if (matches(evt, [keys.Enter, keys.Space])) { this.selectTabAt(index, onSelectionChange); this.setState({ dropdownHidden: true, }); } - }; - }; - - handleTabAnchorFocus = onSelectionChange => { - return index => { - const tabCount = React.Children.count(this.props.children) - 1; - let tabIndex = index; - if (index < 0) { - tabIndex = tabCount; - } else if (index > tabCount) { - tabIndex = 0; - } - - const tab = this.getTabAt(tabIndex); - if (tab) { - this.selectTabAt(tabIndex, onSelectionChange); - if (tab.tabAnchor) { - tab.tabAnchor.focus(); + if (window.matchMedia('(min-width: 42rem)').matches) { + evt.preventDefault(); + const nextIndex = this.getNextIndex(index, this.getDirection(evt)); + const tab = this.getTabAt(nextIndex); + if (tab) { + this.selectTabAt(nextIndex, onSelectionChange); + if (tab.tabAnchor) { + tab.tabAnchor.focus(); + } } } }; @@ -222,7 +241,6 @@ export default class Tabs extends React.Component { index, selected: index === this.state.selected, handleTabClick: this.handleTabClick(onSelectionChange), - handleTabAnchorFocus: this.handleTabAnchorFocus(onSelectionChange), tabIndex, ref: e => { this.setTabAt(index, e);