Skip to content

Commit

Permalink
Add title to gutenberg mobile (#13199)
Browse files Browse the repository at this point in the history
* temporarily disable link formatting

* Make sure RichText resigns focus when unmounted (#13048)

* Implements a native version of post-title.

* Removes some unnecessary log calls.

* Submits a few lint fixes.

* Fixes a linting issue.

* When focusing the title, any focused block loses its focus.

* FocusOut is now wired for post-title for mobile.

* Removes unused some code.

* Added a file I failed to commit.

* Fixes a linting issue.
  • Loading branch information
diegoreymendez authored and youknowriad committed Mar 6, 2019
1 parent 72c0c7b commit a1b2ec9
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* External dependencies
*/
import { includes } from 'lodash';
import { View } from 'react-native';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { createHigherOrderComponent } from '@wordpress/compose';

/**
* Input types which are classified as button types, for use in considering
* whether element is a (focus-normalized) button.
*
* @type {string[]}
*/
const INPUT_BUTTON_TYPES = [
'button',
'submit',
];

/**
* Returns true if the given element is a button element subject to focus
* normalization, or false otherwise.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
*
* @param {Element} element Element to test.
*
* @return {boolean} Whether element is a button.
*/
function isFocusNormalizedButton( element ) {
switch ( element.nodeName ) {
case 'A':
case 'BUTTON':
return true;

case 'INPUT':
return includes( INPUT_BUTTON_TYPES, element.type );
}

return false;
}

export default createHigherOrderComponent(
( WrappedComponent ) => {
return class extends Component {
constructor() {
super( ...arguments );

this.bindNode = this.bindNode.bind( this );
this.cancelBlurCheck = this.cancelBlurCheck.bind( this );
this.queueBlurCheck = this.queueBlurCheck.bind( this );
this.normalizeButtonFocus = this.normalizeButtonFocus.bind( this );
}

componentWillUnmount() {
this.cancelBlurCheck();
}

bindNode( node ) {
if ( node ) {
this.node = node;
} else {
delete this.node;
this.cancelBlurCheck();
}
}

queueBlurCheck( event ) {
// React does not allow using an event reference asynchronously
// due to recycling behavior, except when explicitly persisted.
event.persist();

// Skip blur check if clicking button. See `normalizeButtonFocus`.
if ( this.preventBlurCheck ) {
return;
}

this.blurCheckTimeout = setTimeout( () => {
if ( 'function' === typeof this.node.handleFocusOutside ) {
this.node.handleFocusOutside( event );
}
}, 0 );
}

cancelBlurCheck() {
clearTimeout( this.blurCheckTimeout );
}

/**
* Handles a mousedown or mouseup event to respectively assign and
* unassign a flag for preventing blur check on button elements. Some
* browsers, namely Firefox and Safari, do not emit a focus event on
* button elements when clicked, while others do. The logic here
* intends to normalize this as treating click on buttons as focus.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
*
* @param {MouseEvent} event Event for mousedown or mouseup.
*/
normalizeButtonFocus( event ) {
const { type, target } = event;

const isInteractionEnd = includes( [ 'mouseup', 'touchend' ], type );

if ( isInteractionEnd ) {
this.preventBlurCheck = false;
} else if ( isFocusNormalizedButton( target ) ) {
this.preventBlurCheck = true;
}
}

render() {
// Disable reason: See `normalizeButtonFocus` for browser-specific
// focus event normalization.

/* eslint-disable jsx-a11y/no-static-element-interactions */
return (
<View
onFocus={ this.cancelBlurCheck }
onMouseDown={ this.normalizeButtonFocus }
onMouseUp={ this.normalizeButtonFocus }
onTouchStart={ this.normalizeButtonFocus }
onTouchEnd={ this.normalizeButtonFocus }
onBlur={ this.queueBlurCheck }
>
<WrappedComponent
ref={ this.bindNode }
{ ...this.props } />
</View>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
};
}, 'withFocusOutside'
);
1 change: 1 addition & 0 deletions packages/components/src/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { createSlotFill, Slot, Fill, Provider as SlotFillProvider } from './slot

// Higher-Order Components
export { default as withFilters } from './higher-order/with-filters';
export { default as withFocusOutside } from './higher-order/with-focus-outside';
1 change: 1 addition & 0 deletions packages/editor/src/components/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export { default as BlockFormatControls } from './block-format-controls';
export { default as BlockControls } from './block-controls';
export { default as BlockEdit } from './block-edit';
export { default as DefaultBlockAppender } from './default-block-appender';
export { default as PostTitle } from './post-title';
export { default as EditorHistoryRedo } from './editor-history/redo';
export { default as EditorHistoryUndo } from './editor-history/undo';
84 changes: 84 additions & 0 deletions packages/editor/src/components/post-title/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* External dependencies
*/
import { TextInput } from 'react-native';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { withDispatch } from '@wordpress/data';
import { withFocusOutside } from '@wordpress/components';
import { withInstanceId, compose } from '@wordpress/compose';

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

this.onChange = this.onChange.bind( this );
this.onSelect = this.onSelect.bind( this );
this.onUnselect = this.onUnselect.bind( this );

this.state = {
isSelected: false,
};
}

handleFocusOutside() {
this.onUnselect();
}

onSelect() {
this.setState( { isSelected: true } );
this.props.clearSelectedBlock();
}

onUnselect() {
this.setState( { isSelected: false } );
}

onChange( title ) {
this.props.onUpdate( title );
}

render() {
const {
placeholder,
style,
title,
} = this.props;

const decodedPlaceholder = decodeEntities( placeholder );

return (
<TextInput
blurOnSubmit={ true }
textAlignVertical="top"
multiline
numberOfLines={ 0 }
onChangeText={ this.onChange }
onFocus={ this.onSelect }
placeholder={ decodedPlaceholder }
style={ style }
value={ title }>
</TextInput>
);
}
}

const applyWithDispatch = withDispatch( ( dispatch ) => {
const {
clearSelectedBlock,
} = dispatch( 'core/editor' );

return {
clearSelectedBlock,
};
} );

export default compose(
applyWithDispatch,
withInstanceId,
withFocusOutside
)( PostTitle );

0 comments on commit a1b2ec9

Please sign in to comment.