Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added autoFocus option to EuiTabbedContent #2062

Merged
merged 3 commits into from
Jun 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- Changed `EuiNavDrawerFlyout` title from `h5` to `div` ([#2040](https://github.com/elastic/eui/pull/2040))
- 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