Skip to content

Commit

Permalink
Added autoFocus option to EuiTabbedContent (#2062)
Browse files Browse the repository at this point in the history
... and to the EuiSuperDatePicker implementation
  • Loading branch information
cchaos authored Jun 20, 2019
1 parent 7fe794a commit c107d3a
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Converted `EuiGlobalToastList` into ARIA live region by adding `role="region"` attribute to add NVDA/JAWS support ([#2055](https://github.com/elastic/eui/pull/2055))
- Added `magnifyWithMinus` and `magnifyWithPlus` glyphs to `EuiIcon` ([2056](https://github.com/elastic/eui/pull/2056))
- Added a fully black (no matter the theme) color SASS variable `$euiColorInk` ([2060](https://github.com/elastic/eui/pull/2060))
- Added `autoFocus` prop to `EuiTabbedContent` ([2062](https://github.com/elastic/eui/pull/2062))

**Bug fixes**

Expand Down
1 change: 1 addition & 0 deletions src-docs/src/views/tabs/tabbed_content.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class EuiTabsExample extends Component {
<EuiTabbedContent
tabs={this.tabs}
initialSelectedTab={this.tabs[1]}
autoFocus="selected"
onTabClick={tab => {
console.log('clicked tab', tab);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function EuiDatePopoverContent({
<EuiTabbedContent
className="euiDatePopoverContent"
tabs={renderTabs()}
autoFocus="selected"
initialSelectedTab={{ id: getDateMode(value) }}
onTabClick={onTabClick}
size="s"
Expand Down
3 changes: 3 additions & 0 deletions src/components/tabs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ declare module '@elastic/eui' {
content: ReactNode;
}

type TABBED_CONTENT_AUTOFOCUS = 'initial' | 'selected';

interface EuiTabbedContentProps {
tabs: EuiTabbedContentTab[];
onTabClick?: (tab: EuiTabbedContentTab) => void;
Expand All @@ -37,6 +39,7 @@ declare module '@elastic/eui' {
size?: TAB_SIZES;
display?: TAB_DISPLAYS;
expand?: boolean;
autoFocus?: TABBED_CONTENT_AUTOFOCUS;
}

export const EuiTab: FunctionComponent<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ exports[`EuiTabbedContent behavior when selected tab state isn't controlled by t

exports[`EuiTabbedContent behavior when uncontrolled, the selected tab should update if it receives new content 1`] = `
<EuiTabbedContent
autoFocus="initial"
tabs={
Array [
Object {
Expand All @@ -70,7 +71,9 @@ exports[`EuiTabbedContent behavior when uncontrolled, the selected tab should up
]
}
>
<div>
<div
onFocus={[Function]}
>
<EuiTabs
display="default"
expand={false}
Expand Down Expand Up @@ -199,6 +202,102 @@ exports[`EuiTabbedContent is rendered with required props and tabs 1`] = `
</div>
`;

exports[`EuiTabbedContent props autoFocus initial is rendered 1`] = `
<div>
<div
class="euiTabs"
role="tablist"
>
<button
aria-controls="42"
aria-selected="true"
class="euiTab euiTab-isSelected"
id="es"
role="tab"
type="button"
>
<span
class="euiTab__content"
>
Elasticsearch
</span>
</button>
<button
aria-controls="42"
aria-selected="false"
class="euiTab"
data-test-subj="kibanaTab"
id="kibana"
role="tab"
type="button"
>
<span
class="euiTab__content"
>
Kibana
</span>
</button>
</div>
<div
aria-labelledby="es"
id="42"
role="tabpanel"
>
<p>
Elasticsearch content
</p>
</div>
</div>
`;

exports[`EuiTabbedContent props autoFocus selected is rendered 1`] = `
<div>
<div
class="euiTabs"
role="tablist"
>
<button
aria-controls="42"
aria-selected="true"
class="euiTab euiTab-isSelected"
id="es"
role="tab"
type="button"
>
<span
class="euiTab__content"
>
Elasticsearch
</span>
</button>
<button
aria-controls="42"
aria-selected="false"
class="euiTab"
data-test-subj="kibanaTab"
id="kibana"
role="tab"
type="button"
>
<span
class="euiTab__content"
>
Kibana
</span>
</button>
</div>
<div
aria-labelledby="es"
id="42"
role="tabpanel"
>
<p>
Elasticsearch content
</p>
</div>
</div>
`;

exports[`EuiTabbedContent props display can be condensed 1`] = `
<div>
<div
Expand Down
70 changes: 64 additions & 6 deletions src/components/tabs/tabbed_content/tabbed_content.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types';

import { htmlIdGenerator } from '../../../services';
Expand All @@ -8,6 +8,8 @@ import { EuiTab } from '../tab';

const makeId = htmlIdGenerator();

export const AUTOFOCUS = ['initial', 'selected'];

export class EuiTabbedContent extends Component {
static propTypes = {
className: PropTypes.string,
Expand All @@ -29,6 +31,12 @@ export class EuiTabbedContent extends Component {
* Use this prop if you want to control selection state within the owner component
*/
selectedTab: PropTypes.object,
/**
* When tabbing into the tabs, set the focus on `initial` for the first tab,
* or `selected` for the currently selected tab. Best use case is for inside of
* overlay content like popovers or flyouts.
*/
autoFocus: PropTypes.oneOf(AUTOFOCUS),
size: PropTypes.oneOf(SIZES),
/**
* Each tab needs id and content properties, so we can associate it with its panel for accessibility.
Expand All @@ -49,16 +57,57 @@ export class EuiTabbedContent extends Component {
const { initialSelectedTab, selectedTab, tabs } = props;

this.rootId = makeId();
this.divRef = createRef();

// Only track selection state if it's not controlled externally.
let selectedTabId;
if (!selectedTab) {
this.state = {
selectedTabId:
(initialSelectedTab && initialSelectedTab.id) || tabs[0].id,
};
selectedTabId =
(initialSelectedTab && initialSelectedTab.id) || tabs[0].id;
}

this.state = {
selectedTabId,
inFocus: false,
};
}

componentDidMount() {
// IE11 doesn't support the `relatedTarget` event property for blur events
// but does add it for focusout. React doesn't support `onFocusOut` so here we are.
if (this.divRef.current) {
this.divRef.current.addEventListener('focusout', this.removeFocus);
}
}

componentWillUnmount() {
if (this.divRef.current) {
this.divRef.current.removeEventListener('focusout', this.removeFocus);
}
}

initializeFocus = () => {
if (!this.state.inFocus && this.props.autoFocus === 'selected') {
// Must wait for setState to finish before calling `.focus()`
// as the focus call triggers a blur on the first tab
this.setState({ inFocus: true }, () => {
const targetTab = this.divRef.current.querySelector(
`#${this.state.selectedTabId}`
);
targetTab.focus();
});
}
};

removeFocus = blurEvent => {
// only set inFocus to false if the wrapping div doesn't contain the now-focusing element
if (blurEvent.currentTarget.contains(blurEvent.relatedTarget) === false) {
this.setState({
inFocus: false,
});
}
};

onTabClick = selectedTab => {
const { onTabClick, selectedTab: externalSelectedTab } = this.props;

Expand All @@ -82,6 +131,7 @@ export class EuiTabbedContent extends Component {
selectedTab: externalSelectedTab,
size,
tabs,
autoFocus,
...rest
} = this.props;

Expand All @@ -93,7 +143,11 @@ export class EuiTabbedContent extends Component {
const { content: selectedTabContent, id: selectedTabId } = selectedTab;

return (
<div className={className} {...rest}>
<div
ref={this.divRef}
className={className}
{...rest}
onFocus={this.initializeFocus}>
<EuiTabs expand={expand} display={display} size={size}>
{tabs.map(tab => {
const {
Expand Down Expand Up @@ -125,3 +179,7 @@ export class EuiTabbedContent extends Component {
);
}
}

EuiTabbedContent.defaultProps = {
autoFocus: 'initial',
};
14 changes: 13 additions & 1 deletion src/components/tabs/tabbed_content/tabbed_content.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render, mount } from 'enzyme';
import sinon from 'sinon';
import { requiredProps, findTestSubject } from '../../../test';

import { EuiTabbedContent } from './tabbed_content';
import { EuiTabbedContent, AUTOFOCUS } from './tabbed_content';

// Mock the htmlIdGenerator to generate predictable ids for snapshot tests
jest.mock('../../../services/accessibility/html_id_generator', () => ({
Expand Down Expand Up @@ -81,6 +81,18 @@ describe('EuiTabbedContent', () => {
expect(component).toMatchSnapshot();
});
});

describe('autoFocus', () => {
AUTOFOCUS.forEach(focusType => {
test(`${focusType} is rendered`, () => {
const component = render(
<EuiTabbedContent autoFocus={focusType} tabs={tabs} />
);

expect(component).toMatchSnapshot();
});
});
});
});

describe('behavior', () => {
Expand Down

0 comments on commit c107d3a

Please sign in to comment.