Skip to content

Commit

Permalink
Block Editor: Refactor ObserveTyping as function component (#19881)
Browse files Browse the repository at this point in the history
* Block Editor: Refactor ObserveTyping as function component

* Block Editor: ObserveTyping: Avoid persisting event
  • Loading branch information
aduth authored Jan 27, 2020
1 parent 64767db commit c1266a7
Showing 1 changed file with 48 additions and 92 deletions.
140 changes: 48 additions & 92 deletions packages/block-editor/src/components/observe-typing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { over, includes } from 'lodash';
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { withSelect, withDispatch } from '@wordpress/data';
import { useRef, useEffect } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { isTextField } from '@wordpress/dom';
import {
UP,
Expand All @@ -18,7 +18,7 @@ import {
BACKSPACE,
ESCAPE,
} from '@wordpress/keycodes';
import { withSafeTimeout, compose } from '@wordpress/compose';
import { withSafeTimeout } from '@wordpress/compose';

/**
* Set of key codes upon which typing is to be initiated on a keydown event.
Expand All @@ -41,84 +41,64 @@ function isKeyDownEligibleForStartTyping( event ) {
return ! shiftKey && includes( KEY_DOWN_ELIGIBLE_KEY_CODES, keyCode );
}

class ObserveTyping extends Component {
constructor() {
super( ...arguments );

this.stopTypingOnSelectionUncollapse = this.stopTypingOnSelectionUncollapse.bind( this );
this.stopTypingOnMouseMove = this.stopTypingOnMouseMove.bind( this );
this.startTypingInTextField = this.startTypingInTextField.bind( this );
this.stopTypingOnNonTextField = this.stopTypingOnNonTextField.bind( this );
this.stopTypingOnEscapeKey = this.stopTypingOnEscapeKey.bind( this );

this.onKeyDown = over( [
this.startTypingInTextField,
this.stopTypingOnEscapeKey,
] );

this.lastMouseMove = null;
}

componentDidMount() {
this.toggleEventBindings( this.props.isTyping );
}

componentDidUpdate( prevProps ) {
if ( this.props.isTyping !== prevProps.isTyping ) {
this.toggleEventBindings( this.props.isTyping );
}
}

componentWillUnmount() {
this.toggleEventBindings( false );
}
function ObserveTyping( {
children,
setTimeout: setSafeTimeout,
} ) {
const lastMouseMove = useRef();
const isTyping = useSelect( ( select ) => select( 'core/block-editor' ).isTyping() );
const { startTyping, stopTyping } = useDispatch( 'core/block-editor' );
useEffect( () => {
toggleEventBindings( isTyping );
return () => toggleEventBindings( false );
}, [ isTyping ] );

/**
* Bind or unbind events to the document when typing has started or stopped
* respectively, or when component has become unmounted.
*
* @param {boolean} isBound Whether event bindings should be applied.
*/
toggleEventBindings( isBound ) {
function toggleEventBindings( isBound ) {
const bindFn = isBound ? 'addEventListener' : 'removeEventListener';
document[ bindFn ]( 'selectionchange', this.stopTypingOnSelectionUncollapse );
document[ bindFn ]( 'mousemove', this.stopTypingOnMouseMove );
document[ bindFn ]( 'selectionchange', stopTypingOnSelectionUncollapse );
document[ bindFn ]( 'mousemove', stopTypingOnMouseMove );
}

/**
* On mouse move, unset typing flag if user has moved cursor.
*
* @param {MouseEvent} event Mousemove event.
*/
stopTypingOnMouseMove( event ) {
function stopTypingOnMouseMove( event ) {
const { clientX, clientY } = event;

// We need to check that the mouse really moved because Safari triggers
// mousemove events when shift or ctrl are pressed.
if ( this.lastMouseMove ) {
if ( lastMouseMove.current ) {
const {
clientX: lastClientX,
clientY: lastClientY,
} = this.lastMouseMove;
} = lastMouseMove.current;

if ( lastClientX !== clientX || lastClientY !== clientY ) {
this.props.onStopTyping();
stopTyping();
}
}

this.lastMouseMove = { clientX, clientY };
lastMouseMove.current = { clientX, clientY };
}

/**
* On selection change, unset typing flag if user has made an uncollapsed
* (shift) selection.
*/
stopTypingOnSelectionUncollapse() {
function stopTypingOnSelectionUncollapse() {
const selection = window.getSelection();
const isCollapsed = selection.rangeCount > 0 && selection.getRangeAt( 0 ).collapsed;

if ( ! isCollapsed ) {
this.props.onStopTyping();
stopTyping();
}
}

Expand All @@ -127,9 +107,9 @@ class ObserveTyping extends Component {
*
* @param {KeyboardEvent} event Keypress or keydown event to interpret.
*/
stopTypingOnEscapeKey( event ) {
if ( this.props.isTyping && event.keyCode === ESCAPE ) {
this.props.onStopTyping();
function stopTypingOnEscapeKey( event ) {
if ( isTyping && event.keyCode === ESCAPE ) {
stopTyping();
}
}

Expand All @@ -138,8 +118,7 @@ class ObserveTyping extends Component {
*
* @param {KeyboardEvent} event Keypress or keydown event to interpret.
*/
startTypingInTextField( event ) {
const { isTyping, onStartTyping } = this.props;
function startTypingInTextField( event ) {
const { type, target } = event;

// Abort early if already typing, or key press is incurred outside a
Expand All @@ -156,67 +135,44 @@ class ObserveTyping extends Component {
return;
}

onStartTyping();
startTyping();
}

/**
* Stops typing when focus transitions to a non-text field element.
*
* @param {FocusEvent} event Focus event.
*/
stopTypingOnNonTextField( event ) {
event.persist();
function stopTypingOnNonTextField( event ) {
const { target } = event;

// Since focus to a non-text field via arrow key will trigger before
// the keydown event, wait until after current stack before evaluating
// whether typing is to be stopped. Otherwise, typing will re-start.
this.props.setTimeout( () => {
const { isTyping, onStopTyping } = this.props;
const { target } = event;
setSafeTimeout( () => {
if ( isTyping && ! isTextField( target ) ) {
onStopTyping();
stopTyping();
}
} );
}

render() {
const { children } = this.props;

// Disable reason: This component is responsible for capturing bubbled
// keyboard events which are interpreted as typing intent.

/* eslint-disable jsx-a11y/no-static-element-interactions */
return (
<div
onFocus={ this.stopTypingOnNonTextField }
onKeyPress={ this.startTypingInTextField }
onKeyDown={ this.onKeyDown }
>
{ children }
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
// Disable reason: This component is responsible for capturing bubbled
// keyboard events which are interpreted as typing intent.

/* eslint-disable jsx-a11y/no-static-element-interactions */
return (
<div
onFocus={ stopTypingOnNonTextField }
onKeyPress={ startTypingInTextField }
onKeyDown={ over( [ startTypingInTextField, stopTypingOnEscapeKey ] ) }
>
{ children }
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}

/**
* @see https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/observe-typing/README.md
*/
export default compose( [
withSelect( ( select ) => {
const { isTyping } = select( 'core/block-editor' );

return {
isTyping: isTyping(),
};
} ),
withDispatch( ( dispatch ) => {
const { startTyping, stopTyping } = dispatch( 'core/block-editor' );

return {
onStartTyping: startTyping,
onStopTyping: stopTyping,
};
} ),
withSafeTimeout,
] )( ObserveTyping );
export default withSafeTimeout( ObserveTyping );

0 comments on commit c1266a7

Please sign in to comment.