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);