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: Adding Keyboard Shorcuts to navigate the editor regions #3084

Merged
merged 8 commits into from
Oct 27, 2017
19 changes: 19 additions & 0 deletions components/higher-order/navigate-regions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# navigateRegions

`navigateRegions` is a React [higher-order component](https://facebook.github.io/react/docs/higher-order-components.html) adding keyboard navigation to switch between the different DOM elements marked as "regions" (role="region"). These regions should be focusable (By adding a tabIndex attribute for example)

## Example:

```jsx
function MyLayout() {
return (
<div>
<div role="region" tabIndex="-1">Header</div>
<div role="region" tabIndex="-1">Content</div>
<div role="region" tabIndex="-1">Sidebar</div>
</div>
);
}

export default navigateRegions( MyLayout );
```
77 changes: 77 additions & 0 deletions components/higher-order/navigate-regions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* External Dependencies
*/
import classnames from 'classnames';

/**
* WordPress Dependencies
*/
import { Component } from '@wordpress/element';

/**
* Internal Dependencies
*/
import './style.scss';
import KeyboardShortcuts from '../../keyboard-shortcuts';

function navigateRegions( WrappedComponent ) {
return class extends Component {
constructor() {
super( ...arguments );
this.bindContainer = this.bindContainer.bind( this );
this.focusNextRegion = this.focusRegion.bind( this, 1 );
this.focusPreviousRegion = this.focusRegion.bind( this, -1 );
this.onClick = this.onClick.bind( this );
this.state = {
isFocusingRegions: false,
};
}

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

focusRegion( offset ) {
const regions = [ ...this.container.querySelectorAll( '[role="region"]' ) ];
if ( ! regions.length ) {
return;
}
let nextRegion = regions[ 0 ];
const selectedIndex = regions.indexOf( document.activeElement );
if ( selectedIndex !== -1 ) {
let nextIndex = selectedIndex + offset;
nextIndex = nextIndex === -1 ? regions.length - 1 : nextIndex;
nextIndex = nextIndex === regions.length ? 0 : nextIndex;
nextRegion = regions[ nextIndex ];
}

nextRegion.focus();
this.setState( { isFocusingRegions: true } );
}

onClick() {
this.setState( { isFocusingRegions: false } );
}

render() {
const className = classnames( 'components-navigate-regions', {
'is-focusing-regions': this.state.isFocusingRegions,
} );

// Disable reason: Clicking the editor should dismiss the regions focus style
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
return (
<div ref={ this.bindContainer } className={ className } onClick={ this.onClick }>
<KeyboardShortcuts shortcuts={ {
'ctrl+`': this.focusNextRegion,
'ctrl+shift+`': this.focusPreviousRegion,
} } />
<WrappedComponent { ...this.props } />
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
}
};
}

export default navigateRegions;
12 changes: 12 additions & 0 deletions components/higher-order/navigate-regions/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.components-navigate-regions.is-focusing-regions [role="region"] {
&:focus:after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
border: 4px solid $blue-medium-400;
}
}
1 change: 1 addition & 0 deletions components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export { default as Tooltip } from './tooltip';
export { Slot, Fill, Provider as SlotFillProvider } from './slot-fill';

// Higher-Order Components
export { default as navigateRegions } from './higher-order/navigate-regions';
export { default as withAPIData } from './higher-order/with-api-data';
export { default as withFocusReturn } from './higher-order/with-focus-return';
export { default as withInstanceId } from './higher-order/with-instance-id';
Expand Down
4 changes: 4 additions & 0 deletions editor/assets/stylesheets/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ body.gutenberg-editor-page {
iframe {
width: 100%;
}

.components-navigate-regions {
height: 100%;
}
}

.editor-post-title,
Expand Down
1 change: 1 addition & 0 deletions editor/header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function Header( {
role="region"
aria-label={ __( 'Editor toolbar' ) }
className="editor-header"
tabIndex="-1"
>
<div className="editor-header__content-tools">
<Inserter position="bottom right" />
Expand Down
9 changes: 5 additions & 4 deletions editor/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { NoticeList, Popover } from '@wordpress/components';
import { NoticeList, Popover, navigateRegions } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
Expand All @@ -34,13 +35,13 @@ function Layout( { mode, isSidebarOpened, notices, ...props } ) {
} );

return (
<div key="editor" className={ className }>
<div className={ className }>
<DocumentTitle />
<NoticeList onRemove={ props.removeNotice } notices={ notices } />
<UnsavedChangesWarning />
<AutosaveMonitor />
<Header />
<div className="editor-layout__content">
<div className="editor-layout__content" role="region" aria-label={ __( 'Editor content' ) } tabIndex="-1">
<div className="editor-layout__editor">
{ mode === 'text' && <TextEditor /> }
{ mode === 'visual' && <VisualEditor /> }
Expand All @@ -60,4 +61,4 @@ export default connect(
notices: getNotices( state ),
} ),
{ removeNotice }
)( Layout );
)( navigateRegions( Layout ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks much better, thanks.

1 change: 1 addition & 0 deletions editor/layout/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
}

.editor-layout__content {
position: relative;
display: flex;
flex-direction: column;
}
Expand Down
7 changes: 1 addition & 6 deletions editor/modes/text-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import Textarea from 'react-autosize-textarea';
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { parse } from '@wordpress/blocks';

Expand Down Expand Up @@ -50,11 +49,7 @@ class TextEditor extends Component {
const { value } = this.props;

return (
<div
role="region"
aria-label={ __( 'Editor content' ) }
className="editor-text-editor"
>
<div className="editor-text-editor">
<div className="editor-text-editor__formatting">
<div className="editor-text-editor__formatting-group">
<button className="editor-text-editor__bold">b</button>
Expand Down
7 changes: 2 additions & 5 deletions editor/modes/visual-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { first, last } from 'lodash';
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, findDOMNode } from '@wordpress/element';
import { KeyboardShortcuts } from '@wordpress/components';

Expand Down Expand Up @@ -79,11 +78,9 @@ class VisualEditor extends Component {

render() {
// Disable reason: Clicking the canvas should clear the selection
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
return (
<div
role="region"
aria-label={ __( 'Editor content' ) }
className="editor-visual-editor"
onMouseDown={ this.onClick }
onTouchStart={ this.onClick }
Expand All @@ -104,7 +101,7 @@ class VisualEditor extends Component {
<TableOfContents />
</div>
);
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
}

Expand Down
7 changes: 6 additions & 1 deletion editor/sidebar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import { getActivePanel } from '../selectors';

const Sidebar = ( { panel } ) => {
return (
<div className="editor-sidebar" role="region" aria-label={ __( 'Editor settings' ) }>
<div
className="editor-sidebar"
role="region"
aria-label={ __( 'Editor settings' ) }
tabIndex="-1"
>
<Header />
{ panel === 'document' && <PostSettings key="settings" /> }
{ panel === 'block' && <BlockInspector /> }
Expand Down