diff --git a/components/navigable-menu/README.md b/components/navigable-menu/README.md index 202180a556fa3d..2750f261748c7c 100644 --- a/components/navigable-menu/README.md +++ b/components/navigable-menu/README.md @@ -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 diff --git a/components/navigable-menu/index.js b/components/navigable-menu/index.js index 11ea3cbe1ebb18..d4ce50ee065e52 100644 --- a/components/navigable-menu/index.js +++ b/components/navigable-menu/index.js @@ -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; } diff --git a/editor/block-toolbar/index.js b/editor/block-toolbar/index.js index 2639aed32a2086..6fd3582b07c25b 100644 --- a/editor/block-toolbar/index.js +++ b/editor/block-toolbar/index.js @@ -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 @@ -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'; + +/** + * Module Constants + */ +const { ESCAPE, F10 } = keycodes; function FirstChild( { children } ) { const childrenArray = Children.toArray( children ); return childrenArray[ 0 ] || null; } +function metaKeyPressed( event ) { + 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() { @@ -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; @@ -57,8 +122,14 @@ class BlockToolbar extends Component { transitionLeave={ false } component={ FirstChild } > -