Skip to content

Commit

Permalink
fix(Tabs): ignore disabled tabs on keyboard navigation (#4784)
Browse files Browse the repository at this point in the history
  • Loading branch information
emyarod authored and joshblack committed Jan 13, 2020
1 parent a27fb33 commit 67b2bc2
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 51 deletions.
5 changes: 1 addition & 4 deletions packages/react/src/components/Tab/Tab-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,18 @@ describe('Tab', () => {

describe('keydown', () => {
const onKeyDown = jest.fn();
const handleTabAnchorFocus = jest.fn();
const handleTabKeyDown = jest.fn();
const wrapper = shallow(<Tab label="firstTab" />);
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();
});
});
});
Expand Down
20 changes: 0 additions & 20 deletions packages/react/src/components/Tab/Tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -172,7 +153,6 @@ export default class Tab extends React.Component {
if (disabled) {
return;
}
this.setTabFocus(evt);
handleTabKeyDown(index, evt);
onKeyDown(evt);
}}
Expand Down
9 changes: 6 additions & 3 deletions packages/react/src/components/Tabs/Tabs-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,17 @@ storiesOf('Tabs', module)
<Tab {...props.tab()} label="Tab label 2">
<div className="some-content">Content for second tab goes here.</div>
</Tab>
<Tab {...props.tab()} label="Tab label 3" disabled>
<div className="some-content">Content for third tab goes here.</div>
</Tab>
<Tab
{...props.tab()}
label="Tab label 3"
label="Tab label 4"
renderContent={TabContentRenderedOnlyWhenSelected}>
<div className="some-content">Content for third tab goes here.</div>
<div className="some-content">Content for fourth tab goes here.</div>
</Tab>
<Tab {...props.tab()} label={<CustomLabel text="Custom Label" />}>
<div className="some-content">Content for fourth tab goes here.</div>
<div className="some-content">Content for fifth tab goes here.</div>
</Tab>
</Tabs>
),
Expand Down
62 changes: 60 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,53 @@ describe('Tabs', () => {
expect(wrapper.state().selected).toEqual(1);
});
});

describe('ignore disabled child tab', () => {
let wrapper;
let firstTab;
let lastTab;
beforeEach(() => {
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>
);
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);
});
});
});
});

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 67b2bc2

Please sign in to comment.