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

Accessibility: Consolidate the focusOnOpen behavior in the withFocusReturn Higher Order Component #4566

Closed
wants to merge 1 commit into from
Closed
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
7 changes: 1 addition & 6 deletions components/dropdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import { Component } from '@wordpress/element';
/**
* Internal Dependencies
*/
import withFocusReturn from '../higher-order/with-focus-return';
import Popover from '../popover';

const FocusManaged = withFocusReturn( ( { children } ) => children );

class Dropdown extends Component {
constructor() {
super( ...arguments );
Expand Down Expand Up @@ -88,9 +85,7 @@ class Dropdown extends Component {
onClickOutside={ this.clickOutside }
expandOnMobile={ expandOnMobile }
>
<FocusManaged>
{ renderContent( args ) }
</FocusManaged>
{ renderContent( args ) }
</Popover>
</div>
</div>
Expand Down
19 changes: 19 additions & 0 deletions components/higher-order/with-focus-return/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { focus } from '@wordpress/utils';

/**
* Higher Order Component used to be used to wrap disposable elements like Sidebars, modals, dropdowns.
Expand All @@ -19,12 +20,24 @@ function withFocusReturn( WrappedComponent ) {

this.setIsFocusedTrue = () => this.isFocused = true;
this.setIsFocusedFalse = () => this.isFocused = false;
this.bindContainer = this.bindContainer.bind( this );
}

componentWillMount() {
this.activeElementOnMount = document.activeElement;
}

componentDidMount() {
// Find first tabbable node within content and shift focus, falling
// back to the popover panel itself.
const firstTabbable = focus.tabbable.find( this.container )[ 0 ];
if ( firstTabbable ) {
firstTabbable.focus();
} else {
this.container.focus();
}
}

componentWillUnmount() {
const { activeElementOnMount, isFocused } = this;
if ( ! activeElementOnMount ) {
Expand All @@ -37,11 +50,17 @@ function withFocusReturn( WrappedComponent ) {
}
}

bindContainer( ref ) {
this.container = ref;
}

render() {
return (
<div
onFocus={ this.setIsFocusedTrue }
onBlur={ this.setIsFocusedFalse }
ref={ this.bindContainer }
tabIndex="-1"
>
<WrappedComponent { ...this.props } />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`withFocusReturn() testing rendering and focus handling should focus the container on mount 1`] = `"<div tabindex=\\"-1\\"><div class=\\"test\\">Testing</div></div>"`;

exports[`withFocusReturn() testing rendering and focus handling should pass additional props through to the wrapped element 1`] = `
<_class
data-test="test"
>
<div
onBlur={[Function]}
onFocus={[Function]}
tabIndex="-1"
>
<Test
data-test="test"
>
<div
className="test"
>
Testing
</div>
</Test>
</div>
</_class>
`;

exports[`withFocusReturn() testing rendering and focus handling should render a basic Test component inside the HOC 1`] = `
<_class>
<div
onBlur={[Function]}
onFocus={[Function]}
tabIndex="-1"
>
<Test>
<div
className="test"
>
Testing
</div>
</Test>
</div>
</_class>
`;
25 changes: 12 additions & 13 deletions components/higher-order/with-focus-return/test/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { shallow, mount } from 'enzyme';
import { mount } from 'enzyme';
import { Component } from '../../../../element';

/**
Expand All @@ -10,9 +10,9 @@ import { Component } from '../../../../element';
import withFocusReturn from '../';

class Test extends Component {
render() {
render( props ) {
return (
<div className="test">Testing</div>
<div className="test" { ...props }>Testing</div>
);
}
}
Expand All @@ -32,19 +32,18 @@ describe( 'withFocusReturn()', () => {
} );

it( 'should render a basic Test component inside the HOC', () => {
const renderedComposite = shallow( <Composite /> );
const wrappedElement = renderedComposite.find( 'Test' );
const wrappedElementShallow = wrappedElement.shallow();
expect( wrappedElementShallow.hasClass( 'test' ) ).toBe( true );
expect( wrappedElementShallow.type() ).toBe( 'div' );
expect( wrappedElementShallow.text() ).toBe( 'Testing' );
const renderedComposite = mount( <Composite /> );
expect( renderedComposite ).toMatchSnapshot();
} );

it( 'should pass additional props through to the wrapped element', () => {
const renderedComposite = shallow( <Composite test="test" /> );
const wrappedElement = renderedComposite.find( 'Test' );
// Ensure that the wrapped Test element has the appropriate props.
expect( wrappedElement.props().test ).toBe( 'test' );
const renderedComposite = mount( <Composite data-test="test" /> );
expect( renderedComposite ).toMatchSnapshot();
} );

it( 'should focus the container on mount', () => {
mount( <Composite data-test="test" /> );
expect( document.activeElement.outerHTML ).toMatchSnapshot();
} );

it( 'should not switch focus back to the bound focus element', () => {
Expand Down
29 changes: 1 addition & 28 deletions components/popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { isEqual, noop } from 'lodash';
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { focus, keycodes } from '@wordpress/utils';
import { keycodes } from '@wordpress/utils';

/**
* Internal dependencies
Expand All @@ -35,7 +35,6 @@ class Popover extends Component {
constructor() {
super( ...arguments );

this.focus = this.focus.bind( this );
this.bindNode = this.bindNode.bind( this );
this.getAnchorRect = this.getAnchorRect.bind( this );
this.setOffset = this.setOffset.bind( this );
Expand Down Expand Up @@ -73,10 +72,6 @@ class Popover extends Component {
const { isOpen: prevIsOpen, position: prevPosition } = prevProps;
if ( isOpen !== prevIsOpen ) {
this.toggleWindowEvents( isOpen );

if ( isOpen ) {
this.focus();
}
}

if ( ! isOpen ) {
Expand Down Expand Up @@ -104,27 +99,6 @@ class Popover extends Component {
window[ handler ]( 'scroll', this.throttledSetOffset, true );
}

focus() {
const { focusOnOpen = true } = this.props;
if ( ! focusOnOpen ) {
return;
}

const { content } = this.nodes;
if ( ! content ) {
return;
}

// Find first tabbable node within content and shift focus, falling
// back to the popover panel itself.
const firstTabbable = focus.tabbable.find( content )[ 0 ];
if ( firstTabbable ) {
firstTabbable.focus();
} else {
content.focus();
}
}

throttledSetOffset() {
this.rafHandle = window.requestAnimationFrame( this.setOffset );
}
Expand Down Expand Up @@ -303,7 +277,6 @@ class Popover extends Component {
<div
ref={ this.bindNode( 'content' ) }
className="components-popover__content"
tabIndex="-1"
>
{ children }
</div>
Expand Down
5 changes: 3 additions & 2 deletions components/popover/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@ describe( 'Popover', () => {
it( 'should focus when opening', () => {
// An ideal test here would mount with an input child and focus the
// child, but in context of JSDOM the inputs are not visible and
// are therefore skipped as tabbable, defaulting to popover.
// are therefore skipped as tabbable, defaulting to the popover wrapper.
wrapper = mount( <Popover /> );
wrapper.setProps( { isOpen: true } );

const content = wrapper.find( '.components-popover__content' ).getDOMNode();
// Should focus the withFocusReturn wrapper
const content = wrapper.find( '.components-popover' ).getDOMNode().parentNode;

expect( document.activeElement ).toBe( content );
} );
Expand Down
3 changes: 2 additions & 1 deletion editor/components/post-publish-panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { get } from 'lodash';
*/
import { __ } from '@wordpress/i18n';
import { compose, Component } from '@wordpress/element';
import { withAPIData, IconButton, Spinner } from '@wordpress/components';
import { withAPIData, IconButton, Spinner, withFocusReturn } from '@wordpress/components';

/**
* Internal Dependencies
Expand Down Expand Up @@ -100,4 +100,5 @@ const applyWithAPIData = withAPIData( ( props ) => {
export default compose( [
applyConnect,
applyWithAPIData,
withFocusReturn,
] )( PostPublishPanel );