diff --git a/blocks/rich-text/index.js b/blocks/rich-text/index.js index a256fc21aa53f4..9d3090f21d0642 100644 --- a/blocks/rich-text/index.js +++ b/blocks/rich-text/index.js @@ -17,12 +17,13 @@ import { } from 'lodash'; import { nodeListToReact } from 'dom-react'; import 'element-closest'; +import scrollIntoView from 'dom-scroll-into-view'; /** * WordPress dependencies */ import { createElement, Component, renderToString } from '@wordpress/element'; -import { keycodes, createBlobURL } from '@wordpress/utils'; +import { keycodes, createBlobURL, getScrollContainer } from '@wordpress/utils'; import { withSafeTimeout, Slot, Fill } from '@wordpress/components'; /** @@ -38,6 +39,11 @@ import { EVENTS } from './constants'; const { BACKSPACE, DELETE, ENTER } = keycodes; +/** + * Holds the offset of the root node, to use across instances when needed. + */ +let offsetTop; + export function createTinyMCEElement( type, props, ...children ) { if ( props[ 'data-mce-bogus' ] === 'all' ) { return null; @@ -103,6 +109,8 @@ export class RichText extends Component { this.maybePropagateUndo = this.maybePropagateUndo.bind( this ); this.onPastePreProcess = this.onPastePreProcess.bind( this ); this.onPaste = this.onPaste.bind( this ); + this.onTinyMCEMount = this.onTinyMCEMount.bind( this ); + this.onFocus = this.onFocus.bind( this ); this.state = { formats: {}, @@ -142,6 +150,7 @@ export class RichText extends Component { } ); editor.on( 'init', this.onInit ); + editor.on( 'focusin', this.onFocus ); editor.on( 'focusout', this.onChange ); editor.on( 'NewBlock', this.onNewBlock ); editor.on( 'nodechange', this.onNodeChange ); @@ -204,6 +213,25 @@ export class RichText extends Component { } ); } + onFocus() { + // For virtual keyboards, always scroll the focussed editor into view. + // Unfortunately we cannot detect virtual keyboards, so we check UA. + if ( /iPad|iPhone|iPod|Android/i.test( window.navigator.userAgent ) ) { + const rootNode = this.editor.getBody(); + const rootRect = rootNode.getBoundingClientRect(); + const caretRect = this.editor.selection.getRng().getClientRects()[ 0 ]; + const offset = caretRect ? caretRect.top - rootRect.top : 0; + + scrollIntoView( rootNode, getScrollContainer( rootNode ), { + // Give enough room for toolbar. Must be top. + // Unfortunately we cannot scroll to bottom as the virtual + // keyboard does not change the window size. + offsetTop: 100 - offset, + alignWithTop: true, + } ); + } + } + /** * Handles the global selection change event. */ @@ -559,6 +587,11 @@ export class RichText extends Component { if ( event.shiftKey || ! this.props.onSplit ) { this.editor.execCommand( 'InsertLineBreak', false, event ); } else { + // For type writing offect, save the root node offset so it + // can the position can be scrolled to in the next focussed + // instance. + offsetTop = rootNode.getBoundingClientRect().top; + this.splitContent(); } } @@ -770,6 +803,21 @@ export class RichText extends Component { this.editor.setDirty( true ); } + onTinyMCEMount( node ) { + if ( ! offsetTop ) { + return; + } + + // When a new instance is created, scroll the root node into the + // position of the root node that captured ENTER. + scrollIntoView( node, getScrollContainer( node ), { + offsetTop, + alignWithTop: true, + } ); + + offsetTop = null; + } + render() { const { tagName: Tagname = 'div', @@ -830,6 +878,7 @@ export class RichText extends Component { { ...ariaProps } className={ className } key={ key } + onMount={ this.onTinyMCEMount } /> { isPlaceholderVisible && node.clientHeight ) { - // ...except when overflow is defined to be hidden or visible - const { overflowY } = window.getComputedStyle( node ); - if ( /(auto|scroll)/.test( overflowY ) ) { - return node; - } - } - - // Continue traversing - return getScrollContainer( node.parentNode ); -} - export class BlockListBlock extends Component { constructor() { super( ...arguments ); diff --git a/editor/components/writing-flow/index.js b/editor/components/writing-flow/index.js index 524c4585b39a3e..67974cdb0d4767 100644 --- a/editor/components/writing-flow/index.js +++ b/editor/components/writing-flow/index.js @@ -261,7 +261,12 @@ class WritingFlow extends Component { const blockContainer = this.container.querySelector( `[data-block="${ this.props.selectedBlock.uid }"]` ); if ( blockContainer && ! blockContainer.contains( document.activeElement ) ) { const target = this.getInnerTabbable( blockContainer, this.props.initialPosition === -1 ); - target.focus(); + + // If there is a contenteditable element, it will move focus by itself. + if ( ! target.querySelector( '[contenteditable="true"]' ) ) { + target.focus(); + } + if ( this.props.initialPosition === -1 ) { // Special casing RichText components because the two functions at the bottom are not working as expected. // When merging two sibling paragraph blocks (backspacing) the focus is not moved to the right position. diff --git a/utils/get-scroll-container.js b/utils/get-scroll-container.js new file mode 100644 index 00000000000000..5d32a51585d611 --- /dev/null +++ b/utils/get-scroll-container.js @@ -0,0 +1,24 @@ +/** + * Given a DOM node, finds the closest scrollable container node. + * + * @param {Element} node Node from which to start. + * + * @return {?Element} Scrollable container node, if found. + */ +export function getScrollContainer( node ) { + if ( ! node || node === document.body ) { + return window; + } + + // Scrollable if scrollable height exceeds displayed... + if ( node.scrollHeight > node.clientHeight ) { + // ...except when overflow is defined to be hidden or visible + const { overflowY } = window.getComputedStyle( node ); + if ( /(auto|scroll)/.test( overflowY ) ) { + return node; + } + } + + // Continue traversing + return getScrollContainer( node.parentNode ); +} diff --git a/utils/index.js b/utils/index.js index 99539bea60eb9e..78de6af3faee99 100644 --- a/utils/index.js +++ b/utils/index.js @@ -10,5 +10,6 @@ export { decodeEntities }; export * from './blob-cache'; export * from './mediaupload'; export * from './terms'; +export * from './get-scroll-container'; export { viewPort };