Skip to content

Commit

Permalink
Writing Flow: Fix all the broken flows (#5513)
Browse files Browse the repository at this point in the history
* Block List: Fix select previous on backspace behavior

Regression of #5025, where prop was changed from `previousBlock` to `previousBlockUid`, but neglected to update the instance of the prop reference in keydown handler

* Utils: Improve spec compliancy of text field

* Block: Prefer focus text field on selection, fallback to wrapper

* Writing Flow: Refactor getClosestTabbable to support non-tabbable target

* Block List: Extract typing monitor to separate component

* Block List: Don't deselect block on escape press

Escape doesn't clear focus, so causes problems that block is not selected but retains focus (since isSelected state is synced with focus)

* Block List: Fix delete or insert after focused block wrapper node

* Rich Text: Ensure format toolbar manages its own dismissal

Previously only closed on esc when editing link, not adding new link

TODO: Consolidate editing state
  • Loading branch information
aduth committed Mar 9, 2018
1 parent 0ec3408 commit 07f6ec6
Show file tree
Hide file tree
Showing 12 changed files with 391 additions and 194 deletions.
2 changes: 1 addition & 1 deletion blocks/rich-text/format-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class FormatToolbar extends Component {

onKeyDown( event ) {
if ( event.keyCode === ESCAPE ) {
if ( this.state.isEditingLink ) {
if ( this.state.isEditingLink || this.state.isAddingLink ) {
event.stopPropagation();
this.dropLink();
}
Expand Down
27 changes: 15 additions & 12 deletions edit-post/components/visual-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CopyHandler,
PostTitle,
WritingFlow,
ObserveTyping,
EditorGlobalKeyboardShortcuts,
BlockSelectionClearer,
MultiSelectScrollIntoView,
Expand All @@ -26,18 +27,20 @@ function VisualEditor( { hasFixedToolbar, isLargeViewport } ) {
<EditorGlobalKeyboardShortcuts />
<CopyHandler />
<MultiSelectScrollIntoView />
<WritingFlow>
<PostTitle />
<BlockList
showContextualToolbar={ ! isLargeViewport || ! hasFixedToolbar }
renderBlockMenu={ ( { children, onClose } ) => (
<Fragment>
<BlockInspectorButton onClick={ onClose } />
{ children }
</Fragment>
) }
/>
</WritingFlow>
<ObserveTyping>
<WritingFlow>
<PostTitle />
<BlockList
showContextualToolbar={ ! isLargeViewport || ! hasFixedToolbar }
renderBlockMenu={ ( { children, onClose } ) => (
<Fragment>
<BlockInspectorButton onClick={ onClose } />
{ children }
</Fragment>
) }
/>
</WritingFlow>
</ObserveTyping>
</BlockSelectionClearer>
);
}
Expand Down
165 changes: 35 additions & 130 deletions editor/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Component, findDOMNode, compose } from '@wordpress/element';
import {
keycodes,
focus,
isTextField,
placeCaretAtHorizontalEdge,
placeCaretAtVerticalEdge,
} from '@wordpress/utils';
Expand Down Expand Up @@ -53,8 +54,6 @@ import {
removeBlock,
replaceBlocks,
selectBlock,
startTyping,
stopTyping,
updateBlockAttributes,
toggleSelection,
} from '../../store/actions';
Expand All @@ -75,7 +74,7 @@ import {
getSelectedBlocksInitialCaretPosition,
} from '../../store/selectors';

const { BACKSPACE, ESCAPE, DELETE, ENTER, UP, RIGHT, DOWN, LEFT } = keycodes;
const { BACKSPACE, DELETE, ENTER } = keycodes;

export class BlockListBlock extends Component {
constructor() {
Expand All @@ -86,25 +85,21 @@ export class BlockListBlock extends Component {
this.setAttributes = this.setAttributes.bind( this );
this.maybeHover = this.maybeHover.bind( this );
this.hideHoverEffects = this.hideHoverEffects.bind( this );
this.maybeStartTyping = this.maybeStartTyping.bind( this );
this.stopTypingOnMouseMove = this.stopTypingOnMouseMove.bind( this );
this.mergeBlocks = this.mergeBlocks.bind( this );
this.onFocus = this.onFocus.bind( this );
this.preventDrag = this.preventDrag.bind( this );
this.onPointerDown = this.onPointerDown.bind( this );
this.onKeyDown = this.onKeyDown.bind( this );
this.deleteOrInsertAfterWrapper = this.deleteOrInsertAfterWrapper.bind( this );
this.onBlockError = this.onBlockError.bind( this );
this.insertBlocksAfter = this.insertBlocksAfter.bind( this );
this.onTouchStart = this.onTouchStart.bind( this );
this.onClick = this.onClick.bind( this );
this.selectOnOpen = this.selectOnOpen.bind( this );
this.onSelectionChange = this.onSelectionChange.bind( this );
this.hadTouchStart = false;

this.state = {
error: null,
isHovered: false,
isSelectionCollapsed: true,
};
}

Expand All @@ -126,46 +121,23 @@ export class BlockListBlock extends Component {
}

componentDidMount() {
if ( this.props.isTyping ) {
document.addEventListener( 'mousemove', this.stopTypingOnMouseMove );
}
document.addEventListener( 'selectionchange', this.onSelectionChange );

if ( this.props.isSelected ) {
this.focusTabbable();
}
}

componentWillReceiveProps( newProps ) {
if ( newProps.isTyping || newProps.isSelected ) {
if ( newProps.isTypingWithinBlock || newProps.isSelected ) {
this.hideHoverEffects();
}
}

componentDidUpdate( prevProps ) {
// Bind or unbind mousemove from page when user starts or stops typing
if ( this.props.isTyping !== prevProps.isTyping ) {
if ( this.props.isTyping ) {
document.addEventListener( 'mousemove', this.stopTypingOnMouseMove );
} else {
this.removeStopTypingListener();
}
}

if ( this.props.isSelected && ! prevProps.isSelected ) {
this.focusTabbable();
}
}

componentWillUnmount() {
this.removeStopTypingListener();
document.removeEventListener( 'selectionchange', this.onSelectionChange );
}

removeStopTypingListener() {
document.removeEventListener( 'mousemove', this.stopTypingOnMouseMove );
}

setBlockListRef( node ) {
// Disable reason: The root return element uses a component to manage
// event nesting, but the parent block list layout needs the raw DOM
Expand Down Expand Up @@ -202,15 +174,15 @@ export class BlockListBlock extends Component {
}

// Find all tabbables within node.
const tabbables = focus.tabbable.find( this.node )
.filter( ( node ) => node !== this.node );
const textInputs = focus.tabbable.find( this.node ).filter( isTextField );

// If reversed (e.g. merge via backspace), use the last in the set of
// tabbables.
const isReverse = -1 === initialPosition;
const target = ( isReverse ? last : first )( tabbables );
const target = ( isReverse ? last : first )( textInputs );

if ( ! target ) {
this.wrapperNode.focus();
return;
}

Expand Down Expand Up @@ -297,31 +269,6 @@ export class BlockListBlock extends Component {
}
}

maybeStartTyping() {
// We do not want to dispatch start typing if state value already reflects
// that we're typing (dispatch noise)
if ( ! this.props.isTyping ) {
this.props.onStartTyping();
}
}

stopTypingOnMouseMove( { clientX, clientY } ) {
const { lastClientX, lastClientY } = this;

// We need to check that the mouse really moved
// Because Safari trigger mousemove event when we press shift, ctrl...
if (
lastClientX &&
lastClientY &&
( lastClientX !== clientX || lastClientY !== clientY )
) {
this.props.onStopTyping();
}

this.lastClientX = clientX;
this.lastClientY = clientY;
}

mergeBlocks( forward = false ) {
const { block, previousBlockUid, nextBlockUid, onMerge } = this.props;

Expand All @@ -338,10 +285,6 @@ export class BlockListBlock extends Component {
} else {
onMerge( previousBlockUid, block.uid );
}

// Manually trigger typing mode, since merging will remove this block and
// cause onKeyDown to not fire
this.maybeStartTyping();
}

insertBlocksAfter( blocks ) {
Expand Down Expand Up @@ -406,56 +349,41 @@ export class BlockListBlock extends Component {
}
}

onKeyDown( event ) {
/**
* Interprets keydown event intent to remove or insert after block if key
* event occurs on wrapper node. This can occur when the block has no text
* fields of its own, particularly after initial insertion, to allow for
* easy deletion and continuous writing flow to add additional content.
*
* @param {KeyboardEvent} event Keydown event.
*/
deleteOrInsertAfterWrapper( event ) {
const { keyCode, target } = event;

if ( target !== this.wrapperNode || this.props.isLocked ) {
return;
}

switch ( keyCode ) {
case ENTER:
// Insert default block after current block if enter and event
// not already handled by descendant.
if ( target === this.node && ! this.props.isLocked ) {
event.preventDefault();

this.props.onInsertBlocks( [
createBlock( 'core/paragraph' ),
], this.props.order + 1 );
}

// Pressing enter should trigger typing mode after the content has split
this.maybeStartTyping();
break;

case UP:
case RIGHT:
case DOWN:
case LEFT:
// Arrow keys do not fire keypress event, but should still
// trigger typing mode.
this.maybeStartTyping();
this.props.onInsertBlocks( [
createBlock( 'core/paragraph' ),
], this.props.order + 1 );
event.preventDefault();
break;

case BACKSPACE:
case DELETE:
// Remove block on backspace.
if ( target === this.node ) {
const { uid, onRemove, isLocked, previousBlock, onSelect } = this.props;
event.preventDefault();
if ( ! isLocked ) {
onRemove( uid );

if ( previousBlock ) {
onSelect( previousBlock.uid, -1 );
}
}
}

// Pressing backspace should trigger typing mode
this.maybeStartTyping();
break;
const { uid, onRemove, previousBlockUid, onSelect } = this.props;
onRemove( uid );

case ESCAPE:
// Deselect on escape.
this.props.onDeselect();
if ( previousBlockUid ) {
onSelect( previousBlockUid, -1 );
}
event.preventDefault();
break;
}
}
Expand All @@ -470,19 +398,6 @@ export class BlockListBlock extends Component {
}
}

onSelectionChange() {
if ( ! this.props.isSelected ) {
return;
}

const selection = window.getSelection();
const isCollapsed = selection.rangeCount > 0 && selection.getRangeAt( 0 ).collapsed;
// We only keep track of the collapsed selection for selected blocks.
if ( isCollapsed !== this.state.isSelectionCollapsed && this.props.isSelected ) {
this.setState( { isSelectionCollapsed: isCollapsed } );
}
}

render() {
const {
block,
Expand All @@ -499,6 +414,7 @@ export class BlockListBlock extends Component {
isMultiSelected,
isFirstMultiSelected,
isLastInSelection,
isTypingWithinBlock,
} = this.props;
const isHovered = this.state.isHovered && ! this.props.isMultiSelecting;
const { name: blockName, isValid } = block;
Expand All @@ -508,11 +424,11 @@ export class BlockListBlock extends Component {
// The block as rendered in the editor is composed of general block UI
// (mover, toolbar, wrapper) and the display of the block content.

// If the block is selected and we're typing the block should not appear as selected unless the selection is not collapsed.
// If the block is selected and we're typing the block should not appear.
// Empty paragraph blocks should always show up as unselected.
const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( block );
const isSelectedNotTyping = isSelected && ! isTypingWithinBlock;
const showSideInserter = ( isSelected || isHovered ) && isEmptyDefaultBlock;
const isSelectedNotTyping = isSelected && ( ! this.props.isTyping || ! this.state.isSelectionCollapsed );
const shouldAppearSelected = ! showSideInserter && isSelectedNotTyping;
const shouldShowMovers = shouldAppearSelected || isHovered || ( isEmptyDefaultBlock && isSelectedNotTyping );
const shouldShowSettingsMenu = shouldShowMovers;
Expand Down Expand Up @@ -568,12 +484,11 @@ export class BlockListBlock extends Component {
onTouchStart={ this.onTouchStart }
onFocus={ this.onFocus }
onClick={ this.onClick }
onKeyDown={ this.deleteOrInsertAfterWrapper }
tabIndex="0"
childHandledEvents={ [
'onKeyPress',
'onDragStart',
'onMouseDown',
'onKeyDown',
] }
{ ...wrapperProps }
>
Expand Down Expand Up @@ -602,10 +517,8 @@ export class BlockListBlock extends Component {
{ isFirstMultiSelected && <BlockMultiControls rootUID={ rootUID } /> }
<IgnoreNestedEvents
ref={ this.bindBlockNode }
onKeyPress={ this.maybeStartTyping }
onDragStart={ this.preventDrag }
onMouseDown={ this.onPointerDown }
onKeyDown={ this.onKeyDown }
className="editor-block-list__block-edit"
aria-label={ blockLabel }
data-block={ block.uid }
Expand Down Expand Up @@ -677,7 +590,7 @@ const mapStateToProps = ( state, { uid, rootUID } ) => {
isLastInSelection: state.blockSelection.end === uid,
// We only care about this prop when the block is selected
// Thus to avoid unnecessary rerenders we avoid updating the prop if the block is not selected.
isTyping: isSelected && isTyping( state ),
isTypingWithinBlock: isSelected && isTyping( state ),
order: getBlockIndex( state, uid, rootUID ),
meta: getEditedPostAttribute( state, 'meta' ),
mode: getBlockMode( state, uid ),
Expand All @@ -701,14 +614,6 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( {
dispatch( clearSelectedBlock() );
},

onStartTyping() {
dispatch( startTyping() );
},

onStopTyping() {
dispatch( stopTyping() );
},

onInsertBlocks( blocks, index ) {
const { rootUID, layout } = ownProps;

Expand Down
1 change: 1 addition & 0 deletions editor/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export { default as Inserter } from './inserter';
export { default as MultiBlocksSwitcher } from './block-switcher/multi-blocks-switcher';
export { default as MultiSelectScrollIntoView } from './multi-select-scroll-into-view';
export { default as NavigableToolbar } from './navigable-toolbar';
export { default as ObserveTyping } from './observe-typing';
export { default as PreserveScrollInReorder } from './preserve-scroll-in-reorder';
export { default as Warning } from './warning';
export { default as WritingFlow } from './writing-flow';
Expand Down
Loading

0 comments on commit 07f6ec6

Please sign in to comment.