diff --git a/blocks/rich-text/index.js b/blocks/rich-text/index.js index 6c84e30871d7fe..73f3eb2c45983b 100644 --- a/blocks/rich-text/index.js +++ b/blocks/rich-text/index.js @@ -20,9 +20,10 @@ import 'element-closest'; /** * WordPress dependencies */ -import { createElement, Component, renderToString, Fragment } from '@wordpress/element'; -import { keycodes, createBlobURL, isHorizontalEdge, getRectangleFromRange } from '@wordpress/utils'; +import { createElement, Component, renderToString, Fragment, compose } from '@wordpress/element'; +import { keycodes, createBlobURL, isHorizontalEdge, getRectangleFromRange, getScrollContainer } from '@wordpress/utils'; import { withSafeTimeout, Slot, Fill } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies @@ -415,11 +416,11 @@ export class RichText extends Component { * absolutely position the toolbar. It does this by finding the closest * relative element. * + * @param {DOMRect} position Caret range rectangle. + * * @return {{top: number, left: number}} The desired position of the toolbar. */ - getFocusPosition() { - const position = getRectangleFromRange( this.editor.selection.getRng() ); - + getFocusPosition( position ) { // Find the parent "relative" or "absolute" positioned container const findRelativeParent = ( node ) => { const style = window.getComputedStyle( node ); @@ -529,6 +530,40 @@ export class RichText extends Component { if ( keyCode === BACKSPACE ) { this.onChange(); } + + // `scrollToRect` is called on `nodechange`, whereas calling it on + // `keyup` *when* moving to a new RichText element results in incorrect + // scrolling. Though the following allows false positives, it results + // in much smoother scrolling. + if ( this.props.isViewportSmall && keyCode !== BACKSPACE && keyCode !== ENTER ) { + this.scrollToRect( getRectangleFromRange( this.editor.selection.getRng() ) ); + } + } + + scrollToRect( rect ) { + const { top: caretTop } = rect; + const container = getScrollContainer( this.editor.getBody() ); + + if ( ! container ) { + return; + } + + // When scrolling, avoid positioning the caret at the very top of + // the viewport, providing some "air" and some textual context for + // the user, and avoiding toolbars. + const graceOffset = 100; + + // Avoid pointless scrolling by establishing a threshold under + // which scrolling should be skipped; + const epsilon = 10; + const delta = caretTop - graceOffset; + + if ( Math.abs( delta ) > epsilon ) { + container.scrollTo( + container.scrollLeft, + container.scrollTop + delta, + ); + } } /** @@ -632,8 +667,17 @@ export class RichText extends Component { return accFormats; }, {} ); - const focusPosition = this.getFocusPosition(); + const rect = getRectangleFromRange( this.editor.selection.getRng() ); + const focusPosition = this.getFocusPosition( rect ); + this.setState( { formats, focusPosition, selectedNodeId: this.state.selectedNodeId + 1 } ); + + if ( this.props.isViewportSmall ) { + // Originally called on `focusin`, that hook turned out to be + // premature. On `nodechange` we can work with the finalized TinyMCE + // instance and scroll to proper position. + this.scrollToRect( rect ); + } } updateContent() { @@ -838,4 +882,12 @@ RichText.defaultProps = { formatters: [], }; -export default withSafeTimeout( RichText ); +export default compose( [ + withSelect( ( select ) => { + const { isViewportMatch = identity } = select( 'core/viewport' ) || {}; + return { + isViewportSmall: isViewportMatch( '< small' ), + }; + } ), + withSafeTimeout, +] )( RichText ); diff --git a/components/higher-order/with-filters/README.md b/components/higher-order/with-filters/README.md index bbe7bea3a2af3e..5fc12a0ca0246b 100644 --- a/components/higher-order/with-filters/README.md +++ b/components/higher-order/with-filters/README.md @@ -24,4 +24,4 @@ function MyCustomElement() { export default withFilters( 'MyCustomElement' )( MyCustomElement ); ``` -`withFilters` expects a string argument which provides a hook name. It returns a function which can then be used in composing your component. The hook name allows plugin developers to customize or completely override the component passed to this higher-order component using `wp.utils.addFilter` method. +`withFilters` expects a string argument which provides a hook name. It returns a function which can then be used in composing your component. The hook name allows plugin developers to customize or completely override the component passed to this higher-order component using `wp.hooks.addFilter` method. diff --git a/edit-post/components/layout/style.scss b/edit-post/components/layout/style.scss index 5c46c9ef3b5509..148295b1d46931 100644 --- a/edit-post/components/layout/style.scss +++ b/edit-post/components/layout/style.scss @@ -73,13 +73,18 @@ min-height: 100%; flex-direction: column; - // on mobile the main content area has to scroll - // otherwise you can invoke the overscroll bounce on the non-scrolling container, causing (ノಠ益ಠ)ノ彡┻━┻ + // Pad the scroll box so content on the bottom can be scrolled up. + padding-bottom: 50vh; @include break-small { - overflow-y: auto; - -webkit-overflow-scrolling: touch; + padding-bottom: 0; } + // On mobile the main content area has to scroll otherwise you can invoke + // the overscroll bounce on the non-scrolling container, causing + // (ノಠ益ಠ)ノ彡┻━┻ + overflow-y: auto; + -webkit-overflow-scrolling: touch; + .edit-post-visual-editor { flex-basis: 100%; }