From 2ce31ea899abdbff3830103165d7c17d3d9ecd62 Mon Sep 17 00:00:00 2001 From: emyarod Date: Wed, 27 Nov 2019 00:44:31 -0800 Subject: [PATCH] fix(Tabs): ignore disabled tabs on keyboard navigation --- .../react/src/components/Tabs/Tabs-test.js | 57 ++++++++++++++++- packages/react/src/components/Tabs/Tabs.js | 62 ++++++++++++------- 2 files changed, 95 insertions(+), 24 deletions(-) diff --git a/packages/react/src/components/Tabs/Tabs-test.js b/packages/react/src/components/Tabs/Tabs-test.js index 55f5034c7032..793821e153d6 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,48 @@ describe('Tabs', () => { expect(wrapper.state().selected).toEqual(1); }); }); + + describe('ignore disabled child tab', () => { + const wrapper = mount( + + + content1 + + + content2 + + + content3 + + + ); + const firstTab = wrapper.find('.firstTab').last(); + const 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);