Skip to content

Commit

Permalink
Add A11y Navigation Mode (#16500)
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad authored and gziolo committed Aug 29, 2019
1 parent 110104e commit cb8f1f0
Show file tree
Hide file tree
Showing 24 changed files with 348 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,18 @@ _Returns_

- `boolean`: True if multi-selecting, false if not.

<a name="isNavigationMode" href="#isNavigationMode">#</a> **isNavigationMode**

Returns whether the navigation mode is enabled.

_Parameters_

- _state_ `Object`: Editor state.

_Returns_

- `boolean`: Is navigation mode enabled.

<a name="isSelectionEnabled" href="#isSelectionEnabled">#</a> **isSelectionEnabled**

Selector that returns if multi-selection is enabled or not.
Expand Down Expand Up @@ -1071,6 +1083,18 @@ _Parameters_

- _clientId_ `string`: Block client ID.

<a name="setNavigationMode" href="#setNavigationMode">#</a> **setNavigationMode**

Returns an action object used to enable or disable the navigation mode.

_Parameters_

- _isNavigationMode_ `string`: Enable/Disable navigation mode.

_Returns_

- `Object`: Action object

<a name="setTemplateValidity" href="#setTemplateValidity">#</a> **setTemplateValidity**

Returns an action object resetting the template validity.
Expand Down
97 changes: 69 additions & 28 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import { animated } from 'react-spring/web.cjs';
/**
* WordPress dependencies
*/
import { useRef, useEffect, useState } from '@wordpress/element';
import { useRef, useEffect, useLayoutEffect, useState } from '@wordpress/element';
import {
focus,
isTextField,
placeCaretAtHorizontalEdge,
} from '@wordpress/dom';
import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes';
import { BACKSPACE, DELETE, ENTER, ESCAPE } from '@wordpress/keycodes';
import {
getBlockType,
getSaveElement,
Expand Down Expand Up @@ -101,6 +101,8 @@ function BlockListBlock( {
onSelectionStart,
animateOnChange,
enableAnimation,
isNavigationMode,
enableNavigationMode,
} ) {
// Random state used to rerender the component if needed, ideally we don't need this
const [ , updateRerenderState ] = useState( {} );
Expand All @@ -118,6 +120,8 @@ function BlockListBlock( {
// Hovered area of the block
const hoverArea = useHoveredArea( wrapper );

const breadcrumb = useRef();

// Keep track of touchstart to disable hover on iOS
const hadTouchStart = useRef( false );
const onTouchStart = () => {
Expand Down Expand Up @@ -215,6 +219,11 @@ function BlockListBlock( {
return;
}

if ( isNavigationMode ) {
breadcrumb.current.focus();
return;
}

// Find all tabbables within node.
const textInputs = focus.tabbable
.find( blockNodeRef.current )
Expand Down Expand Up @@ -254,6 +263,18 @@ function BlockListBlock( {
// Block Reordering animation
const animationStyle = useMovingAnimation( wrapper, isSelected || isPartOfMultiSelection, enableAnimation, animateOnChange );

// Focus the breadcrumb if the wrapper is focused on navigation mode.
// Focus the first editable or the wrapper if edit mode.
useLayoutEffect( () => {
if ( isSelected ) {
if ( isNavigationMode ) {
breadcrumb.current.focus();
} else {
focusTabbable( true );
}
}
}, [ isSelected, isNavigationMode ] );

// Other event handlers

/**
Expand All @@ -275,32 +296,43 @@ function BlockListBlock( {
*
* @param {KeyboardEvent} event Keydown event.
*/
const deleteOrInsertAfterWrapper = ( event ) => {
const onKeyDown = ( event ) => {
const { keyCode, target } = event;

// These block shortcuts should only trigger if the wrapper of the block is selected
// And when it's not a multi-selection to avoid conflicting with RichText/Inputs and multiselection.
if (
! isSelected ||
target !== wrapper.current ||
isLocked
) {
return;
}
// ENTER/BACKSPACE Shortcuts are only available if the wrapper is focused
// and the block is not locked.
const canUseShortcuts = (
isSelected &&
! isLocked &&
( target === wrapper.current || target === breadcrumb.current )
);
const isEditMode = ! isNavigationMode;

switch ( keyCode ) {
case ENTER:
// Insert default block after current block if enter and event
// not already handled by descendant.
onInsertDefaultBlockAfter();
event.preventDefault();
if ( canUseShortcuts && isEditMode ) {
// Insert default block after current block if enter and event
// not already handled by descendant.
onInsertDefaultBlockAfter();
event.preventDefault();
}
break;

case BACKSPACE:
case DELETE:
// Remove block on backspace.
onRemove( clientId );
event.preventDefault();
if ( canUseShortcuts ) {
// Remove block on backspace.
onRemove( clientId );
event.preventDefault();
}
break;
case ESCAPE:
if (
isSelected &&
isEditMode
) {
enableNavigationMode();
wrapper.current.focus();
}
break;
}
};
Expand Down Expand Up @@ -357,8 +389,8 @@ function BlockListBlock( {

// If the block is selected and we're typing the block should not appear.
// Empty paragraph blocks should always show up as unselected.
const showInserterShortcuts = ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid;
const showEmptyBlockSideInserter = ( isSelected || isHovered || isLast ) && isEmptyDefaultBlock && isValid;
const showInserterShortcuts = ! isNavigationMode && ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid;
const showEmptyBlockSideInserter = ! isNavigationMode && ( isSelected || isHovered || isLast ) && isEmptyDefaultBlock && isValid;
const shouldAppearSelected =
! isFocusMode &&
! showEmptyBlockSideInserter &&
Expand All @@ -371,20 +403,23 @@ function BlockListBlock( {
! isEmptyDefaultBlock;
// We render block movers and block settings to keep them tabbale even if hidden
const shouldRenderMovers =
! isNavigationMode &&
( isSelected || hoverArea === ( isRTL ? 'right' : 'left' ) ) &&
! showEmptyBlockSideInserter &&
! isPartOfMultiSelection &&
! isTypingWithinBlock;
const shouldShowBreadcrumb =
! isFocusMode && isHovered && ! isEmptyDefaultBlock;
( isSelected && isNavigationMode ) ||
( ! isNavigationMode && ! isFocusMode && isHovered && ! isEmptyDefaultBlock );
const shouldShowContextualToolbar =
! isNavigationMode &&
! hasFixedToolbar &&
! showEmptyBlockSideInserter &&
(
( isSelected && ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) ||
isFirstMultiSelected
);
const shouldShowMobileToolbar = shouldAppearSelected;
const shouldShowMobileToolbar = ! isNavigationMode && shouldAppearSelected;

// Insertion point can only be made visible if the block is at the
// the extent of a multi-selection, or not in a multi-selection.
Expand All @@ -399,6 +434,7 @@ function BlockListBlock( {
{
'has-warning': ! isValid || !! hasError || isUnregisteredBlock,
'is-selected': shouldAppearSelected,
'is-navigate-mode': isNavigationMode,
'is-multi-selected': isPartOfMultiSelection,
'is-hovered': shouldAppearHovered,
'is-reusable': isReusableBlock( blockType ),
Expand Down Expand Up @@ -464,7 +500,7 @@ function BlockListBlock( {
onTouchStart={ onTouchStart }
onFocus={ onFocus }
onClick={ onTouchStop }
onKeyDown={ deleteOrInsertAfterWrapper }
onKeyDown={ onKeyDown }
tabIndex="0"
aria-label={ blockLabel }
childHandledEvents={ [ 'onDragStart', 'onMouseDown' ] }
Expand Down Expand Up @@ -509,9 +545,7 @@ function BlockListBlock( {
{ shouldShowBreadcrumb && (
<BlockBreadcrumb
clientId={ clientId }
isHidden={
! ( isHovered || isSelected ) || hoverArea !== ( isRTL ? 'right' : 'left' )
}
ref={ breadcrumb }
/>
) }
{ ( shouldShowContextualToolbar || isForcingContextualToolbar.current ) && (
Expand All @@ -522,6 +556,7 @@ function BlockListBlock( {
/>
) }
{
! isNavigationMode &&
! shouldShowContextualToolbar &&
isSelected &&
! hasFixedToolbar &&
Expand Down Expand Up @@ -604,6 +639,7 @@ const applyWithSelect = withSelect(
getBlockIndex,
getBlockOrder,
__unstableGetBlockWithoutInnerBlocks,
isNavigationMode,
} = select( 'core/block-editor' );
const block = __unstableGetBlockWithoutInnerBlocks( clientId );
const isSelected = isBlockSelected( clientId );
Expand Down Expand Up @@ -637,6 +673,7 @@ const applyWithSelect = withSelect(
isFocusMode: focusMode && isLargeViewport,
hasFixedToolbar: hasFixedToolbar && isLargeViewport,
isLast: index === blockOrder.length - 1,
isNavigationMode: isNavigationMode(),
isRTL,

// Users of the editor.BlockListBlock filter used to be able to access the block prop
Expand Down Expand Up @@ -664,6 +701,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => {
mergeBlocks,
replaceBlocks,
toggleSelection,
setNavigationMode,
} = dispatch( 'core/block-editor' );

return {
Expand Down Expand Up @@ -737,6 +775,9 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => {
toggleSelection( selectionEnabled ) {
toggleSelection( selectionEnabled );
},
enableNavigationMode() {
setNavigationMode( true );
},
};
} );

Expand Down
86 changes: 27 additions & 59 deletions packages/block-editor/src/components/block-list/breadcrumb.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { Toolbar } from '@wordpress/components';
import { withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { Toolbar, Button } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { forwardRef } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -17,62 +16,31 @@ import BlockTitle from '../block-title';
* the root block.
*
* @param {string} props.clientId Client ID of block.
* @param {string} props.rootClientId Client ID of block's root.
* @param {Function} props.selectRootBlock Callback to select root block.
* @return {WPElement} Block Breadcrumb.
*/
export class BlockBreadcrumb extends Component {
constructor() {
super( ...arguments );
this.state = {
isFocused: false,
const BlockBreadcrumb = forwardRef( ( { clientId }, ref ) => {
const { setNavigationMode } = useDispatch( 'core/block-editor' );
const { rootClientId } = useSelect( ( select ) => {
return {
rootClientId: select( 'core/block-editor' ).getBlockRootClientId( clientId ),
};
this.onFocus = this.onFocus.bind( this );
this.onBlur = this.onBlur.bind( this );
}

onFocus( event ) {
this.setState( {
isFocused: true,
} );

// This is used for improved interoperability
// with the block's `onFocus` handler which selects the block, thus conflicting
// with the intention to select the root block.
event.stopPropagation();
}

onBlur() {
this.setState( {
isFocused: false,
} );
}

render() {
const { clientId, rootClientId } = this.props;

return (
<div className={ 'editor-block-list__breadcrumb block-editor-block-list__breadcrumb' }>
<Toolbar>
{ rootClientId && (
<>
<BlockTitle clientId={ rootClientId } />
<span className="editor-block-list__descendant-arrow block-editor-block-list__descendant-arrow" />
</>
) }
} );

return (
<div className="editor-block-list__breadcrumb block-editor-block-list__breadcrumb">
<Toolbar>
{ rootClientId && (
<>
<BlockTitle clientId={ rootClientId } />
<span className="editor-block-list__descendant-arrow block-editor-block-list__descendant-arrow" />
</>
) }
<Button ref={ ref } onClick={ () => setNavigationMode( false ) }>
<BlockTitle clientId={ clientId } />
</Toolbar>
</div>
);
}
}

export default compose( [
withSelect( ( select, ownProps ) => {
const { getBlockRootClientId } = select( 'core/block-editor' );
const { clientId } = ownProps;
</Button>
</Toolbar>
</div>
);
} );

return {
rootClientId: getBlockRootClientId( clientId ),
};
} ),
] )( BlockBreadcrumb );
export default BlockBreadcrumb;
Loading

0 comments on commit cb8f1f0

Please sign in to comment.