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

Block Editor: Refactor ObserveTyping as function component #19881

Merged
merged 2 commits into from
Jan 27, 2020
Merged
Changes from 1 commit
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
137 changes: 47 additions & 90 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';
aduth marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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,45 @@ 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 ) {
function stopTypingOnNonTextField( event ) {
event.persist();
aduth marked this conversation as resolved.
Show resolved Hide resolved

// 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;
setSafeTimeout( () => {
const { target } = event;
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 );