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

Support Navigation and Edit Mode #16500

Merged
merged 29 commits into from
Aug 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
69dc40d
Support Navigation and Edit Mode
youknowriad Jul 10, 2019
56c1f4e
Support removing blocks from navigation mode
youknowriad Jul 10, 2019
df415d5
Add is-navigation className
youknowriad Jul 10, 2019
fc8510a
Style adjustments
kjellr Jul 10, 2019
fe42bc2
Update API docs
youknowriad Jul 10, 2019
e3db2a5
Fix e2e tests by moving to edit mode
youknowriad Jul 10, 2019
1af2e64
alt+f10 only works on edit mode
youknowriad Jul 10, 2019
494bec0
Use blue highlight color all around.
kjellr Jul 11, 2019
58e51f9
no hover styles and breadcrumbs on hover in navigation mode
youknowriad Jul 11, 2019
836a6e5
Fix image navigation mode
youknowriad Jul 15, 2019
6662db5
Trigger the edit mode on mouse down instead of move
youknowriad Jul 18, 2019
c9469e6
Fix the edit mode trigger in e2e tests
youknowriad Jul 18, 2019
e11c08d
Correct breadcrumb size and text position.
kjellr Jul 19, 2019
680c254
Rename the is-navigate-mode className and fix the docs
youknowriad Jul 24, 2019
1ee2661
Fix typo
youknowriad Jul 24, 2019
439c30f
Better param documentation
youknowriad Jul 24, 2019
1903f3d
Fix typo
youknowriad Jul 24, 2019
21cb1ad
Fix typo
youknowriad Jul 24, 2019
4eb1cf2
Simplify the arrow navigation code
youknowriad Jul 24, 2019
3af2e23
Fix ESCAPE behavior
youknowriad Jul 24, 2019
ad94bd7
Fix e2e test edit mode utility
youknowriad Jul 26, 2019
fcae4c1
Simplify applied classname
youknowriad Jul 29, 2019
4de1964
Update packages/e2e-test-utils/src/keyboard-mode.js
youknowriad Jul 29, 2019
226a8c8
Use a boolean instead of a string for the keyboard mode
youknowriad Jul 29, 2019
039efc0
Adding comment
youknowriad Jul 29, 2019
e75a720
Fix e2e tests
youknowriad Jul 29, 2019
5400dcf
More consistency in the naming of the navigation mode actions/functions
youknowriad Aug 2, 2019
aae87af
Limit the edit mode trigger to the writing flow component
youknowriad Aug 2, 2019
fbfbe6c
Consistently disable navigation mode in e2e tests
youknowriad Aug 2, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 &&
Copy link
Member

Choose a reason for hiding this comment

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

What's the reason we want to prevent switching modes when the template is locked? Isn't it still useful to be able to navigate content, even if the arrangement of said content can't be modified?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That boolean is not used for mode switching. So it should still be possible.

( 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 ) {
Copy link
Member

Choose a reason for hiding this comment

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

Minor: these nested switch/if statements look a bit odd to me. Could they maybe be combined?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No easy way to do it. We could just use if elses?

// 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