diff --git a/components/higher-order/navigate-regions/README.md b/components/higher-order/navigate-regions/README.md new file mode 100644 index 0000000000000..f93568558f48a --- /dev/null +++ b/components/higher-order/navigate-regions/README.md @@ -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 ( +
+
Header
+
Content
+
Sidebar
+
+ ); +} + +export default navigateRegions( MyLayout ); +``` \ No newline at end of file diff --git a/components/higher-order/navigate-regions/index.js b/components/higher-order/navigate-regions/index.js new file mode 100644 index 0000000000000..f060b5494af95 --- /dev/null +++ b/components/higher-order/navigate-regions/index.js @@ -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 ( +
+ + +
+ ); + /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ + } + }; +} + +export default navigateRegions; diff --git a/components/higher-order/navigate-regions/style.scss b/components/higher-order/navigate-regions/style.scss new file mode 100644 index 0000000000000..edf682bf0729c --- /dev/null +++ b/components/higher-order/navigate-regions/style.scss @@ -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; + } +} diff --git a/components/index.js b/components/index.js index f9b22dc1dbc9e..f05e64cd5ac57 100644 --- a/components/index.js +++ b/components/index.js @@ -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'; diff --git a/editor/assets/stylesheets/main.scss b/editor/assets/stylesheets/main.scss index bf980881ddcb1..2ed0d41352c1d 100644 --- a/editor/assets/stylesheets/main.scss +++ b/editor/assets/stylesheets/main.scss @@ -74,6 +74,10 @@ body.gutenberg-editor-page { iframe { width: 100%; } + + .components-navigate-regions { + height: 100%; + } } .editor-post-title, diff --git a/editor/header/index.js b/editor/header/index.js index 5d48cab2311e1..ae9c17332a985 100644 --- a/editor/header/index.js +++ b/editor/header/index.js @@ -35,6 +35,7 @@ function Header( { role="region" aria-label={ __( 'Editor toolbar' ) } className="editor-header" + tabIndex="-1" >
diff --git a/editor/layout/index.js b/editor/layout/index.js index 3f6eb13ded988..a3ffbfde124a4 100644 --- a/editor/layout/index.js +++ b/editor/layout/index.js @@ -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 @@ -34,13 +35,13 @@ function Layout( { mode, isSidebarOpened, notices, ...props } ) { } ); return ( -
+
-
+
{ mode === 'text' && } { mode === 'visual' && } @@ -60,4 +61,4 @@ export default connect( notices: getNotices( state ), } ), { removeNotice } -)( Layout ); +)( navigateRegions( Layout ) ); diff --git a/editor/layout/style.scss b/editor/layout/style.scss index c2eaba24fdcea..a62db2ad9b682 100644 --- a/editor/layout/style.scss +++ b/editor/layout/style.scss @@ -8,6 +8,7 @@ } .editor-layout__content { + position: relative; display: flex; flex-direction: column; } diff --git a/editor/modes/text-editor/index.js b/editor/modes/text-editor/index.js index 846efa57c71f9..6b9327d0f6c59 100644 --- a/editor/modes/text-editor/index.js +++ b/editor/modes/text-editor/index.js @@ -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'; @@ -50,11 +49,7 @@ class TextEditor extends Component { const { value } = this.props; return ( -
+
diff --git a/editor/modes/visual-editor/index.js b/editor/modes/visual-editor/index.js index 2f3744c607ab2..d1edd5a1becb0 100644 --- a/editor/modes/visual-editor/index.js +++ b/editor/modes/visual-editor/index.js @@ -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'; @@ -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 (
); - /* 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 */ } } diff --git a/editor/sidebar/index.js b/editor/sidebar/index.js index 88449b8b57671..f3b2e773ce345 100644 --- a/editor/sidebar/index.js +++ b/editor/sidebar/index.js @@ -21,7 +21,12 @@ import { getActivePanel } from '../selectors'; const Sidebar = ( { panel } ) => { return ( -
+
{ panel === 'document' && } { panel === 'block' && }