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: Keyboard shortcuts to navigate to/from/in the block's toolbar #2960

Merged
merged 7 commits into from
Oct 13, 2017
8 changes: 8 additions & 0 deletions components/navigable-menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ A callback invoked when the menu navigates to one of its children passing the in

- Type: `Function`
- Required: No

## deep

A boolean to look for navigable children in the direct children or any descendant.

- Type: `Boolean`
- Required: No
- default: false
6 changes: 3 additions & 3 deletions components/navigable-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ class NavigableMenu extends Component {
}

onKeyDown( event ) {
const { orientation = 'vertical', onNavigate = noop } = this.props;
const { orientation = 'vertical', onNavigate = noop, deep = false } = this.props;
if (
( orientation === 'vertical' && [ UP, DOWN, TAB ].indexOf( event.keyCode ) === -1 ) ||
( orientation === 'horizontal' && [ RIGHT, LEFT, TAB ].indexOf( event.keyCode ) === -1 )
) {
return;
}

const tabbables = focus.tabbable
.find( this.container )
.filter( ( node ) => node.parentElement === this.container );
.filter( ( node ) => deep || node.parentElement === this.container );
const indexOfTabbable = tabbables.indexOf( document.activeElement );

if ( indexOfTabbable === -1 ) {
return;
}
Expand Down
81 changes: 76 additions & 5 deletions editor/block-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import classnames from 'classnames';
/**
* WordPress Dependencies
*/
import { IconButton, Toolbar } from '@wordpress/components';
import { Component, Children } from '@wordpress/element';
import { IconButton, Toolbar, NavigableMenu } from '@wordpress/components';
import { Component, Children, findDOMNode } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { focus, keycodes } from '@wordpress/utils';

/**
* Internal Dependencies
Expand All @@ -19,19 +20,51 @@ import './style.scss';
import BlockSwitcher from '../block-switcher';
import BlockMover from '../block-mover';
import BlockRightMenu from '../block-settings-menu';
import { isMac } from '../utils/dom';
Copy link
Member

Choose a reason for hiding this comment

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

While it uses DOM properties to determine, isMac doesn't seem like a DOM-specific utility function.


/**
* Module Constants
*/
const { ESCAPE, F10 } = keycodes;

function FirstChild( { children } ) {
const childrenArray = Children.toArray( children );
return childrenArray[ 0 ] || null;
}

function metaKeyPressed( event ) {
Copy link
Member

Choose a reason for hiding this comment

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

Would it have been possible to use the KeyboardShortcuts component to manage and normalize these keyboard combinations?

https://github.com/WordPress/gutenberg/tree/master/components/keyboard-shortcuts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried but it failed to trigger on "command" key only, not sure why

Copy link
Member

Choose a reason for hiding this comment

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

I tried but it failed to trigger on "command" key only, not sure why

Might have been running into caveat of Mousetrap not firing callbacks while input is focused. See #3031 for workaround.

return isMac() ? event.metaKey : ( event.ctrlKey && ! event.altKey );
}

class BlockToolbar extends Component {
constructor() {
super( ...arguments );
this.toggleMobileControls = this.toggleMobileControls.bind( this );
this.bindNode = this.bindNode.bind( this );
this.onKeyUp = this.onKeyUp.bind( this );
this.onKeyDown = this.onKeyDown.bind( this );
this.onToolbarKeyDown = this.onToolbarKeyDown.bind( this );
this.state = {
showMobileControls: false,
};

// it's not easy to know if the user only clicked on a "meta" key without simultaneously clicking on another key
// We keep track of the key counts to ensure it's reliable
this.metaCount = 0;
}

componentDidMount() {
document.addEventListener( 'keyup', this.onKeyUp );
document.addEventListener( 'keydown', this.onKeyDown );
}

componentWillUnmount() {
document.removeEventListener( 'keyup', this.onKeyUp );
document.removeEventListener( 'keydown', this.onKeyDown );
}

bindNode( ref ) {
this.toolbar = findDOMNode( ref );
}

toggleMobileControls() {
Expand All @@ -40,6 +73,38 @@ class BlockToolbar extends Component {
} ) );
}

onKeyDown( event ) {
if ( metaKeyPressed( event ) ) {
this.metaCount++;
}
}

onKeyUp( event ) {
const shouldFocusToolbar = this.metaCount === 1 || ( event.keyCode === F10 && event.altKey );
this.metaCount = 0;

if ( shouldFocusToolbar ) {
const tabbables = focus.tabbable.find( this.toolbar );
if ( tabbables.length ) {
tabbables[ 0 ].focus();
}
}
}

onToolbarKeyDown( event ) {
if ( event.keyCode !== ESCAPE ) {
return;
}

// Is there a better way to focus the selected block
// TODO: separate focused/selected block state and use Redux actions instead
const selectedBlock = document.querySelector( '.editor-visual-editor__block.is-selected .editor-visual-editor__block-edit' );
if ( !! selectedBlock ) {
event.stopPropagation();
selectedBlock.focus();
}
}

render() {
const { showMobileControls } = this.state;
const { uid } = this.props;
Expand All @@ -57,8 +122,14 @@ class BlockToolbar extends Component {
transitionLeave={ false }
component={ FirstChild }
>
<div className={ toolbarClassname }>
<div className="editor-block-toolbar__group">
<NavigableMenu
className={ toolbarClassname }
ref={ this.bindNode }
orientation="horizontal"
role="toolbar"
deep
>
<div className="editor-block-toolbar__group" onKeyDown={ this.onToolbarKeyDown }>
{ ! showMobileControls && [
<BlockSwitcher key="switcher" uid={ uid } />,
<Slot key="slot" name="Formatting.Toolbar" />,
Expand All @@ -80,7 +151,7 @@ class BlockToolbar extends Component {
}
</Toolbar>
</div>
</div>
</NavigableMenu>
</CSSTransitionGroup>
);
}
Expand Down
9 changes: 9 additions & 0 deletions editor/utils/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,12 @@ export function placeCaretAtEdge( container, start = false ) {
sel.addRange( range );
container.focus();
}

/**
Copy link
Member

Choose a reason for hiding this comment

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

Thanks 🙇

* Checks whether the user is on MacOS or not
*
* @return {Boolean} Is Mac or Not
*/
export function isMac() {
return window.navigator.platform.toLowerCase().indexOf( 'mac' ) !== -1;
}
2 changes: 2 additions & 0 deletions utils/keycodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export const UP = 38;
export const RIGHT = 39;
export const DOWN = 40;
export const DELETE = 46;

export const F10 = 121;
Copy link
Member

Choose a reason for hiding this comment

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

Nice, we should have all keycodes here to make code easier to read 👍