Skip to content

Commit

Permalink
fix(Tabs): ignore disabled tabs on keyboard navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
emyarod committed Nov 27, 2019
1 parent 160bf6e commit 26eda89
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 24 deletions.
57 changes: 55 additions & 2 deletions packages/react/src/components/Tabs/Tabs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<div>)', () => {
Expand Down Expand Up @@ -241,6 +252,48 @@ describe('Tabs', () => {
expect(wrapper.state().selected).toEqual(1);
});
});

describe('ignore disabled child tab', () => {
const wrapper = mount(
<Tabs>
<Tab label="firstTab" className="firstTab">
content1
</Tab>
<Tab label="middleTab" className="middleTab" disabled>
content2
</Tab>
<Tab label="lastTab" className="lastTab">
content3
</Tab>
</Tabs>
);
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);
});
});
});
});

Expand Down
62 changes: 40 additions & 22 deletions packages/react/src/components/Tabs/Tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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}`]) ||
Expand All @@ -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();
}
}
}
};
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 26eda89

Please sign in to comment.