-
Notifications
You must be signed in to change notification settings - Fork 4.7k
RichText: Keep caret visible when typing on mobile #5769
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
Changes from all commits
31b28b8
ccfaac0
11876e6
174900b
c2d5abd
433d556
ff5bdf6
cf1729b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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( | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm finding this is not very durable, or at least I'm finding some browser inconsistencies. The document.body.scrollTo( 0, 500 );I expect nothing will happen. ... That is, of course, unless you're running Safari or, more precisely, iOS Safari (though desktop works just as well), where it will scroll. I'm not familiar with why this is different, though I suspect that when there is no explicit scroll container, Chrome and Firefox expect scrolls to be made via Though the fact that If we expect to call
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Thanks for digging. The fact is I haven't tested this on Android yet, as I currently don't own any such device.
All that sounds good, I'll see how much we can get out of the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apologies for not testing this sooner — I only tested on iPhone and in the browser and it worked fine there. I retested, and in the Android emulator it's working pretty damn well for me: I also tested using the Chrome "mobile simulator" (which is just desktop chrome with a small viewport and fancy mobile scrollbars, so it's not accurate), but I got a few JS errors there: Let me know how I can help with any other testing. |
||||||
| 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' ) || {}; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are the fallbacks here for? Why
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Buuuuuuump.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't think of a good reason. We should remove that. Trying the new Suggestion feature!
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
😞
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I approve 😄 |
||||||
| return { | ||||||
| isViewportSmall: isViewportMatch( '< small' ), | ||||||
| }; | ||||||
| } ), | ||||||
| withSafeTimeout, | ||||||
| ] )( RichText ); | ||||||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we check to see whether the desired location is already within viewport, even if it's not necessarily at the "top"? It's odd when clicking around on blocks in desktop that the viewport jumps constantly, even when the blocks are already visible in screen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yesterday I got lost in a rabbit hole with this, discovering strange edge cases, issues on desktop. I was initially happy as I thought I'd nicely add this to all devices, but now I'm thinking we should retain the is-mobile-device filter from the parent PR...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if it may be fine to have also on desktop, it would be nice to wrap an interim version of this PR because the improvements it means for mobile are so huge. If it means disabling it for desktop and potentially revisiting for desktop in the future, that's okay I feel.