-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
RichText: Keep caret visible when typing on mobile #5769
Conversation
blocks/rich-text/index.js
Outdated
window.scrollTo( | ||
window.pageXOffset, | ||
window.pageYOffset + caretHeight - toolbarOffset | ||
); |
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.
scrollIntoView
was behaving quite awkwardly for me, scrolling a bit unpredictably. I'd still rather use whatever our preferred interface is, at least one that looks at scrollable containers. For the sake of having something demoable I pushed this anyway (and there's something nice about just calling window.scrollTo
).
@@ -28,6 +28,7 @@ | |||
// Collapse to minimum height of 50px, to fully occupy editor bottom pad. | |||
height: 50px; | |||
width: $content-width; | |||
max-width: 100%; |
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.
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.
Rebasing this. Seems addressed in #5945. Dropping this line. Correct me if I'm wrong.
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.
I misunderstood Miguel's initial comment here, but yes, changing to move the fixed value from the width to max-width should be better here. I.e.
width: 100%;
max-width: $content-width;
should be right.
Holy guacamole, thank you so much for working on this. Holy moly guacamole IT'S WORKING IT'S WORKING https://media.giphy.com/media/9K2nFglCAQClO/200.gif GIF: This cannot be merged in fast enough. SHIP IT SHIP IT! I can't believe you two did it. Ella proved it was possible and nearly fixed it. This takes it home. While I imagine future improvements can be made, none of them feel critical at all to me, as this is working so well for me. 👏 👏 👏 👏 👏 👏 👏 👏 🔥 |
blocks/rich-text/index.js
Outdated
const caretRect = this.getEditorSelectionRect(); | ||
const caretHeight = caretRect.y; | ||
if ( caretHeight !== this.caretHeight ) { | ||
const toolbarOffset = 100; |
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.
Which toolbar does this refer to? And should the RichText component have built-in knowledge of said toolbar's height? When we redesign the toolbar, who will know to update this value accordingly?
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.
And should the RichText component have built-in knowledge of said toolbar's height?
It shouldn't. :)
Which toolbar does this refer to?
I grabbed the name from a comment in the parent PR, but I actually think of it independently of any toolbars. It's more of a "grace offset" so that we don't scroll to place the caret at the very top of the viewport: by leaving some empty space, we can incidentally accommodate toolbars, but it's more about scrolling to position the caret in a more central place, and perhaps revealing the end of the previous block to provide context.
When we redesign the toolbar, who will know to update this value accordingly?
From my previous reasoning, there's no imperative to synchronize the values. That said, it is a magic constant, and is disconnected from the toolbar. Other ways of addressing include computing the offset dynamically, e.g. 1/3 · viewportHeight
.
Until further feedback, I'll rename and comment to clarify the intent.
blocks/rich-text/index.js
Outdated
|
||
scrollToCaret() { | ||
const caretRect = this.getEditorSelectionRect(); | ||
const caretHeight = caretRect.y; |
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.
Is it accurate to label this as a height value? Based on .y
, I'd think it's a point offset.
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.
Actually, what isn't accurate is to grab .y
, since
[1] IE and Edge support the non-standard MSDN: ClientRect which does not define x and y properties (#)
in non-IE, .y
and .top
hold the same value* for caretRect
, so I'll just switch the property. Thanks for asking.
*: Returns the top coordinate value of the DOMRect (has the same value as y, or y + height if height is negative.)
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.
Edit: I'd written height
when I meant top
🙈 . Updated.
blocks/rich-text/index.js
Outdated
scrollToCaret() { | ||
const caretRect = this.getEditorSelectionRect(); | ||
const caretHeight = caretRect.y; | ||
if ( caretHeight !== this.caretHeight ) { |
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.
This compares to an instance value, meaning on its first invocation, it will always call window.scrollTo
. Do we need the instance value, or can we just compare against the desired pageYOffset
to see if we need to perform a scroll?
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.
Good point. Actually, one thing we can do is:
if ( caretHeight !== this.caretHeight ) {
const delta = caretTop - toolbarOffset;
if ( Math.abs( delta ) > epsilon ) {
window.scrollTo( window.pageXOffset, window.pageYOffset + delta );
}
}
where epsilon is as an arbitrarily small number, e.g. 10.
This solves the problem of pointless scrolling more generally.
blocks/rich-text/index.js
Outdated
if ( Math.abs( delta ) > epsilon ) { | ||
window.scrollTo( | ||
window.pageXOffset, | ||
window.pageYOffset + caretTop - graceOffset |
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.
Minor: You could reuse delta
here, window.pageYOffset + delta
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.
I was wondering why I was seeing unpredictable behavior on larger viewports. It seems specific to the fact that we assume window is the overflow area we want to scroll within. In desktop, the editor canvas is the scrollable region. Worse yet, desktop still tries to scroll (at least at shorter viewports where a scroll exists), but does so on the wrong element.
Should we use utils.dom.getScrollContainer
?
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.
Yeah, I meant to circle back to getScrollContainer
before merging, sorry it wasn't clear.
Minor: You could reuse delta here, window.pageYOffset + delta
D'oh, that was the idea.
blocks/rich-text/index.js
Outdated
scrollToCaret() { | ||
const caretRect = this.getEditorSelectionRect(); | ||
const caretTop = caretRect.top; | ||
if ( caretTop !== this.caretTop ) { |
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.
Still not clear why we need this condition or the this.caretTop
instance variable.
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.
My original thinking was that, when typing in a line, this would be a cheaper test than computing delta
from window.pageYOffset
. However, now reading the documentation for window.scrollY
, of which pageYOffset
is an alias, there's no mention of any incurred cost of reading this property. In fact, it reads:
Use this property to check that the document hasn't already been scrolled when using relative scroll functions such as scrollBy(), scrollByLines(), or scrollByPages().
Which is enough to convince me to drop this eq check.
blocks/rich-text/index.js
Outdated
@@ -558,6 +558,38 @@ export class RichText extends Component { | |||
if ( keyCode === BACKSPACE ) { | |||
this.onChange(); | |||
} | |||
|
|||
// `scrollToCaret` is called on `nodechange`, whereas calling it on | |||
// `keyup` *when* moving to a new RichText element results in incorrect |
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.
Why do we assume backspace or enter would result in a new RichText element? Doesn't this depend on presence of onSplit
and onMerge
props?
Wondering also if this is a behavior that ought to be handled by individual instances of RichText, vs. a global handler (WritingFlow?)
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.
Doesn't this depend on presence of onSplit and onMerge props?
It does; I've been trying out different heuristics but wanted to maintain momentum. Not sure what to converge on. In practice, I'm seeing good behaviors on paragraph, quote and list, which cover a decent set of multiline / onSplit / onMerge
combinations.
WritingFlow?
On one hand, it makes architectural sense; on the other, RichText may be the best entity to track the caret in its own editor
, so I'm not sure. It'd be great to have this in the next release, though, which is why I followed the approach of the parent PR.
blocks/rich-text/index.js
Outdated
// 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 = this.props.isViewportSmall ? 100 : 300; |
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.
I wasn't very with reading clientHeight
on every key release, and also with the fact that it was blindly grabbing a third of the height when we don't know what the device actually looks like, so I went with this.
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.
I wasn't very with reading
clientHeight
on every key release
Perhaps I'm just uninformed, but what's the reason to think this is not a trivial property access?
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.
I was trying to find a reference, but maybe I dreamt the whole thing. I thought I recalled Paul Lewis from Google, who often talks about performance, bringing up certain read interfaces of the DOM that were more expensive than one would suspect. I was looking here but didn't find anything. Maybe it's totally fine.
Anyway, per my other comment about rolling this out for mobile only, do you think it's still worth having clientHeight / 3.0
, or would the magic 100
be enough?
One limitation right now is that there's no scrolling when a user presses I'd rather look at that later. The primary purpose of this PR was to improve the mobile experience, where by default no external keyboards are used and thus where |
This is super key, and thanks again for working on this. |
blocks/rich-text/index.js
Outdated
// 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 = this.props.isViewportSmall ? 100 : 300; |
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.
I wasn't very with reading
clientHeight
on every key release
Perhaps I'm just uninformed, but what's the reason to think this is not a trivial property access?
-webkit-overflow-scrolling: touch; | ||
} | ||
// Pad the scroll box so content on the bottom can be scrolled up. | ||
padding-bottom: 50vh; |
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.
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.
D'oh, thanks. I tried placing the padding in other places, but they end up affecting mobile scrolling (though not this PR's), presumably due to the addition of a scrolling container. Since I'd like to retain this feature for mobile only and I'm short of ideas, I've pushed f1a4108.
const epsilon = 10; | ||
const delta = caretTop - graceOffset; | ||
|
||
if ( Math.abs( delta ) > epsilon ) { |
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.
9b258f7
to
411d187
Compare
Let's get this in and let folks test ahead of release. What do you say, @aduth? |
const delta = caretTop - graceOffset; | ||
|
||
if ( Math.abs( delta ) > epsilon ) { | ||
container.scrollTo( |
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.
I'm finding this is not very durable, or at least I'm finding some browser inconsistencies. The container
here will be document.body
. If you open your browser's Developer Tools console right now (on GitHub.com) and enter:
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 window.scrollTo
(which does scroll).
Though the fact that getScrollContainer
is returning document.body
could itself be concerning since, as mentioned, it's not an explicit scroll container. It does qualify for document.body.scrollHeight > document.body.clientHeight
. Maybe it's because there's an overflow-x: hidden
? I've had some previous experience with issues where even one direction of overflow
has effects in both directions.
If we expect to call scrollTo
on the result of getScrollableContainer
, then I think we ought to normalize so that if the browser must call window.scrollTo
on a non-explicit container, then getScrollableContainer
should return window
. This means either improving the detection of a scrollable container, or returning window
when the result would otherwise be document.body
.
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.
I'm finding this is not very durable […] That is, of course, unless you're running Safari or, more precisely, iOS Safari (though desktop works just as well), where it will scroll.
Thanks for digging. The fact is I haven't tested this on Android yet, as I currently don't own any such device.
If we expect to call
scrollTo
on the result ofgetScrollableContainer
, […]
All that sounds good, I'll see how much we can get out of the body->window
normalization.
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.
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.
blocks/rich-text/index.js
Outdated
return; | ||
} | ||
|
||
const { top: caretTop } = this.getEditorSelectionRect(); |
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.
I think getFocusPosition
is now equivalent to getEditorSelectionRect
? So we could pass that in nodeChange
?
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.
I'm a bit tied up today, but if you're interested and have spare time, I'd be grateful if you had a go. :)
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.
Sure!
blocks/rich-text/index.js
Outdated
// scrolling. Though the following allows false positives, it results | ||
// in much smoother scrolling. | ||
if ( keyCode !== BACKSPACE && keyCode !== ENTER ) { | ||
this.scrollToCaret(); |
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.
Do we really want to run this stuff on every keystroke, down the line: https://gist.github.com/paulirish/5d52fb081b3570c81e3a#range.
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.
Or maybe this is fine if there are no layout changes.
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.
That's the list I was looking for the other week!
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.
Yeah but the layout is not invalidated... so this seems fine. I don't see any issues atm.
blocks/rich-text/index.js
Outdated
|
||
const { top: caretTop } = this.getEditorSelectionRect(); | ||
|
||
const container = getScrollContainer( this.editor.getBody() ); |
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.
Could this be cached?
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.
I'd say yes, but what would invalidate it? Resizing? Viewport breakpoints? Or do we assume the container to be constant throughout RichText's lifetime?
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.
Oh, right, I somehow thought it would remain constant.
* Change graceOffset to a third of the viewport height, to see how it feels.
f1a4108
to
cf1729b
Compare
Rebased and addressed #5769 (comment). While testing on iOS Safari I run into some strange issues... When I focus the text all text sometimes goes blank... Can't reproduce consistently and probably unrelated to this but just in case someone else experiences this... |
Gave it a spin and my previous issues are gone. Wasn't able to easily test in the simulator, but assuming that hasn't regressed since I tested earlier today, this is 👍 👍 to ship. The chief reason to ship this is that it brings huge improvements to iOS which frankly is in a somewhat sorry state right now. Even if it turns out that there are some aspects of this PR that aren't perfect, it might still be worth shipping this and fixing separately. THANK YOU for working on this, Ella and Miguel. It's sooo much better than before. |
🔥 🔥 🔥 🔥 🔥 🔥 WOOOOOOOOOOO ƪ(˘⌣˘)┐ ƪ(˘⌣˘)ʃ ┌(˘⌣˘)ʃ (ノ^v^)ノ彡┻━┻ |
export default withSafeTimeout( RichText ); | ||
export default compose( [ | ||
withSelect( ( select ) => { | ||
const { isViewportMatch = identity } = select( 'core/viewport' ) || {}; |
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.
What are the fallbacks here for? Why = identity
? Why || {}
?
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.
What are the fallbacks here for? Why
= identity
? Why|| {}
?
Buuuuuuump.
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.
I can't think of a good reason. We should remove that. Trying the new Suggestion feature!
const { isViewportMatch = identity } = select( 'core/viewport' ) || {}; | |
const { isViewportMatch } = select( 'core/viewport' ); |
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.
Suggestions cannot be applied while the pull request is closed.
😞
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.
I approve 😄
Description
A smaller iteration of #4775. Partly fixes #4731.
This PR is a spin-off of #4775 that focuses on keeping in sight the area into which a user is typing. The secondary goal is to leave a small footprint and reducing exceptions (e.g. per viewport width, per user agent).
A notable departure from #4775 is that this PR doesn't just cover Enter presses, but also line wraps — i.e., when enough text is typed that it is placed on a new line in the editor. This aggressive change warrants some extra testing, but, in my own testing, is a welcome addition.
How Has This Been Tested?
multiline
blocks (e.g. Quote) and in non-multiline (e.g. Paragraph).Screenshots (jpeg or gifs if applicable):
Types of changes
Checklist: