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

Improve arrow nav (horizontal only) #2431

Closed
wants to merge 6 commits into from
Closed
Changes from all commits
Commits
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
158 changes: 110 additions & 48 deletions editor/modes/visual-editor/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import classnames from 'classnames';
import { Slot } from 'react-slot-fill';
import { partial } from 'lodash';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import tinymce from 'tinymce';

/**
* WordPress dependencies
Expand Down Expand Up @@ -58,6 +59,69 @@ function FirstChild( { children } ) {
return childrenArray[ 0 ] || null;
}

function isEdge( { editor, reverse } ) {
const position = reverse ? 'start' : 'end';
const order = reverse ? 'first' : 'last';
const range = editor.selection.getRng();
const offset = range[ `${ position }Offset` ];
const rootNode = editor.getBody();
let node = range.startContainer;

if ( ! range.collapsed ) {
return false;
}

if ( reverse && offset !== 0 ) {
return false;
}

if ( ! reverse && offset !== node.textContent.length ) {
return false;
}

while ( node !== rootNode ) {
const parentNode = node.parentNode;

if ( parentNode[ `${ order }Child` ] !== node ) {
return false;
}

node = parentNode;
}

return true;
}

function getNextEditor( { node, target, reverse } ) {
const selector = '.editor-visual-editor [contenteditable="true"]';
const editableNodes = Array.from( document.querySelectorAll( selector ) );

if ( reverse ) {
editableNodes.reverse();
}

const index = editableNodes.indexOf( target );
const nextEditableNode = editableNodes[ index + 1 ];
const direction = reverse ? 'previous' : 'next';
const nextBlockNode = node[ `${ direction }Sibling` ];

if ( ! nextBlockNode || ! nextEditableNode ) {
return;
}

const editor = tinymce.get( nextEditableNode.id );

if ( ! editor ) {
return;
}

if ( ! node.contains( nextEditableNode ) && ! nextBlockNode.contains( nextEditableNode ) ) {
return;
}

return editor;
}

class VisualEditorBlock extends Component {
constructor() {
super( ...arguments );
Expand All @@ -73,7 +137,6 @@ class VisualEditorBlock extends Component {
this.onPointerDown = this.onPointerDown.bind( this );
this.onKeyDown = this.onKeyDown.bind( this );
this.onKeyUp = this.onKeyUp.bind( this );
this.handleArrowKey = this.handleArrowKey.bind( this );
this.toggleMobileControls = this.toggleMobileControls.bind( this );
this.onBlockError = this.onBlockError.bind( this );

Expand Down Expand Up @@ -251,12 +314,54 @@ class VisualEditorBlock extends Component {

onKeyDown( event ) {
const { keyCode, target } = event;
const { previousBlock, nextBlock, onFocus } = this.props;
const up = keyCode === UP;
const down = keyCode === DOWN;
const left = keyCode === LEFT;
const right = keyCode === RIGHT;

if ( up || down || left || right ) {
Copy link
Member

Choose a reason for hiding this comment

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

Pyramid of if could be improved if we delegated this to some helper methods that had early returns (as previously with handleArrowKey). See below comment too where we have early return that will inadvertently skip other onKeyDown logic.

const editor = target.id && tinymce.get( target.id );

if ( target === this.node ) {
const reverse = up || left;
const followingBlock = reverse ? previousBlock : nextBlock;

if ( followingBlock ) {
event.preventDefault();
onFocus( followingBlock.uid, { offset: reverse ? -1 : 0 } );
}
}

if ( editor ) {
const reverse = up || left;
const followingBlock = reverse ? previousBlock : nextBlock;

if ( ! isEdge( { editor, reverse } ) ) {
return;
Copy link
Member

Choose a reason for hiding this comment

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

A return here makes me wary since we don't isolate this logic from other onKeyDown handlers. Currently the only remaining logic which would be skipped is the ENTER handling, that would never coincide with this, but seems easy to overlook in future maintenance.

}

event.preventDefault();

this.handleArrowKey( event );
const followingEditor = getNextEditor( { node: this.node, target, reverse } );

if ( keyCode === UP || keyCode === LEFT || keyCode === DOWN || keyCode === RIGHT ) {
const selection = window.getSelection();
this.lastRange = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
if ( ! followingEditor ) {
if ( followingBlock ) {
onFocus( followingBlock.uid, { offset: reverse ? -1 : 0 } );
}

return;
}

followingEditor.focus();

if ( reverse ) {
followingEditor.selection.select( followingEditor.getBody(), true );
Copy link
Member

Choose a reason for hiding this comment

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

Is the reason we get TinyMCE instance primarily to have access to its APIs for selecting the end of the next block? Native APIs aren't quite as nice, but not absurd:

const range = document.createRange();
range.selectNodeContents( followingEditor );
range.collapse( false );
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange( range );

I think I might be more okay with this when we pull out and isolate these behaviors, as in #2424.

followingEditor.selection.collapse( false );
} else {
followingEditor.selection.setCursorLocation();
Copy link
Member

Choose a reason for hiding this comment

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

What does this do? Can we comment as such?

}
}
}

if ( ENTER === keyCode && target === this.node ) {
Expand All @@ -270,49 +375,6 @@ class VisualEditorBlock extends Component {

onKeyUp( event ) {
this.removeOrDeselect( event );
this.handleArrowKey( event );
}

handleArrowKey( event ) {
const { keyCode, target } = event;
const moveUp = ( keyCode === UP || keyCode === LEFT );
const moveDown = ( keyCode === DOWN || keyCode === RIGHT );
const wrapperClassname = '.editor-visual-editor';
const selectors = [
'*[contenteditable="true"]',
'*[tabindex]',
'textarea',
'input',
].map( ( selector ) => `${ wrapperClassname } ${ selector }` ).join( ',' );

if ( moveUp || moveDown ) {
const selection = window.getSelection();
const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;

// If there's no movement, so we're either at the end of start, or
// no text input at all.
if ( range !== this.lastRange ) {
return;
}

const focusableNodes = Array.from( document.querySelectorAll( selectors ) );

if ( moveUp ) {
focusableNodes.reverse();
}

const targetNode = focusableNodes
.slice( focusableNodes.indexOf( target ) )
.reduce( ( result, node ) => {
return result || ( node.contains( target ) ? null : node );
}, null );

if ( targetNode ) {
targetNode.focus();
}
}

delete this.lastRange;
}

toggleMobileControls() {
Expand Down