-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Vertical arrow key navigation #2988
Conversation
8c5ac78
to
204884e
Compare
|
71002ed
to
2a19bfc
Compare
|
3190dcc
to
cd6d238
Compare
Getting quite happy with this. Squashed down a few other bugs. On rare occasions the browser fails to move position, have yet to figure out these few edge cases. |
cd6d238
to
0b41ee3
Compare
Fixed an issue where the new range is not within the target container. |
0b41ee3
to
af6d188
Compare
Encountered one more bug with the button block. It will attempt to put the caret next to it but fail. :) Will fix in a bit. |
This seems to be working QUITE well. Impressive. Lovely. As you get near the end of the demo content, the page seems to scroll in jumps, so to speak. This can make it a little easy to lose focus of where the caret is. This is very possibly just normal browser behavior, in which case ignore crazy old Joen. But otherwise — what could this be and is there a way to make it smoother, perhaps that typewriter approach? |
Inside the code block, the range stuff doesn't work. Which is totally fine, it doesn't have to — but figured I'd mention it in case it's like a one line code change. |
I think that might be the bug I mentioned. I know you'd like that typewriter feel in general, but let's attempt that in a separate PR. 😄 |
Totally, no worries.
No worries — this enhancement is way more useful for the editables. Doesn't have to work elsewhere imo. |
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.
Here's a first review. I think we should tackle the matter of firstVerticalRect
.
I will now switch context for a bit so that I can come back to this later with a clearer head and see if I can help.
editor/writing-flow/index.js
Outdated
return null; | ||
} | ||
|
||
return node; |
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 noticed this usage of reduce
just the other day in master
, and my question still stands in this PR. The above method call can be modelled as:
xs.reduce( ( result, x, i ) => result || ( pred( x, i ) ? x : null )
which to me looks like Array#find
in disguise (and more expensive). Is it not?
// import { find } from 'lodash'
find( xs, pred )
Feel free to correct me if I missed something. :)
editor/writing-flow/index.js
Outdated
const focusableNodes = this.getVisibleTabbables(); | ||
if ( direction === 'UP' ) { | ||
|
||
if ( reverse ) { |
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 though this.getVisibleTabbables
will return a new instance on each call and we can see that detail in this very component, I tend to be careful with in-place modifications like Array#reverse
. I'll leave it up to you, but I would either use lodash#reverse
or even [ ...focusableNodes ].reverse()
.
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 don't really see the problem here? Should I change it?
editor/utils/dom.js
Outdated
const rangeRect = | ||
range.startContainer.nodeType === window.Node.ELEMENT_NODE ? | ||
range.startContainer.getBoundingClientRect() : | ||
range.getClientRects()[ 0 ]; |
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 point, but I find that multi-line ternaries read better like:
a
? b
: c
but currently our linter has no preference.
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.
Fair enough. I've been used to this way. Maybe we should add a rule so that it's the same everywhere?
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 only worry is that the Javascript interpreter of some browser out there will see the condition as the end of the statement if the question mark is not the same line. It may be changed in transpiling though so if Babble gets it right then I suppose I don't need to worry.
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 probably why I've been used to it this way.
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 had a whole reply with an excerpt of the ECMA spec and an analysis making the case that only a faulty browser would be too eager to insert a semicolon in this case, but then I found the right Crockford source. :)
The ternary operator can be visually confusing, so ? question mark always begins a line and increase the indentation by 4 spaces, and : colon always begins a line, aligned with the ? question mark. The condition should be wrapped in parens.
My take is that Crockford has historically been a conservative reference on JS (and, apparently, a previous version of the same text had advised against having an operator at the beginning of a line), and thus his OK means that we're totally safe here and that it's ultimately a stylistic decision. How do y'all weigh in?
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 don't have a strong opinion. If browsers are a worry, we anyway process with Babel and everything gets minified.
editor/utils/dom.js
Outdated
// Editable was already focussed, it goes back to old range... | ||
// This fixes it. | ||
selection.removeAllRanges(); | ||
selection.addRange( 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.
Could you provide more detail on the need to repeat this? Is it a quirk?, is it something otherwise preventable?, etc.
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 suspect it might be related to TinyMCE's editor.selection.lastFocusBookmark
, but I need to investigate. Maybe @ephox-mogran or @EphoxJames know 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.
I don't know off the top of my head but I will dig into the code and see if I can find out.
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.
Thanks! Ideally we wouldn't have to touch the TinyMCE editor instance at all here.
editor/utils/dom.js
Outdated
|
||
const range = caretRangeFromPoint( document, x, y ); | ||
|
||
container.style.zIndex = null; |
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.
Hm, bummer. :)
Local testing (on this very page) indicates that container.style.pointerEvents = 'none';
is just as effective. Should we prefer that one instead? I wonder if that way we minimize our chances of generating visual artefacts.
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.
How would that be used? On the toolbar element? That would require us to have the toolbar element, which would be less isolated logic. In that case I'd prefer the z-index approach.
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.
Yes, it would be on the toolbar — I was looking at this so closely that I didn't weigh the drawback of having to deal with yet another entity. z-index may be the lesser evil, then.
editor/utils/dom.js
Outdated
* | ||
* @type {DOMRect} | ||
*/ | ||
let firstVerticalRect; |
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 biggest concern is this global here. It's a concern in general, but aggravated by the fact that we are in a utils
module, where the expectation of lingering state is low, and that the main function that sets it is a seemingly innocent predicate, isVerticalEdge
.
I see it as a definite code smell that we should tackle, probably before merging. Does it mean that the predicate should become something else? That it should be broken into different pieces? That it should become a function returning a DOMRect
or null
? Relatedly, onMouseDown={ resetVerticalPosition }
on WritingFlow is an implementation detail that shouldn't be leaking.
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.
Any ideas would be welcome! I'll have a think.
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.
Can firstVerticalRect just be passed in, and have its state / value managed in WritingFlow itself? It is essentially part of the state of writing flow, if that's where we are doing the cursor movement.
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.
Okay, I changed this.
editor/utils/dom.js
Outdated
*/ | ||
export function placeCaretAtEdge( container, start = false ) { | ||
export function isVerticalEdge( container, reverse ) { | ||
if ( [ 'INPUT', 'TEXTAREA' ].indexOf( container.tagName ) !== -1 ) { |
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: naming this, as is done in other parts of the diff, would be nice: const isInputOrTextarea =
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 almost changed the other one. :) What makes you prefer it?
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 indexOf
isn't very descriptive and the variable assignment counters that. I would also go, instead, for lodash#includes
.
editor/writing-flow/index.js
Outdated
const right = keyCode === RIGHT; | ||
const reverse = up || left; | ||
const horizontal = left || right; | ||
const vertical = up || down; |
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.
As some of the functions here and in dom/utils
grow, keeping track of boolean variables becomes harder. Aside from a [modal]+[main verb]
nomenclature (with modal taking values in { is, should, can, may, will, … }), I'd love to see more descriptive names throughout. Examples:
up
→isUpKey
reverse
→isAscending
noScroll
→ logic reversal, becomingmayUseScroll
@iseulde I've found one issue that might be making it a little inconsistent. It's most visible when using the table block. If you setup a paragraph below the table and after the table, and try to navigate from below the table up, or above the table down, then the caret doesn't stay in the same horizontal position when it is entering the table. This is because of two reasons:
In order to offset this, you can run the caretRangeFromPosition method again, but this time, use an (x, y) that is bounded within the Now, you may get a problem where it will just keep recursing indefinitely, because it can't find a text position that is any better than it's first guess. Therefore, you should quit after some number of retries. It shouldn't be very many. Note, the cursor definitely doesn't keep the same position when moving through the table itself, but that should be logged against TinyMCE.
|
55ebf3c
to
5f731cd
Compare
673c0ac
to
f4d2700
Compare
@iseulde: a224863 fixes an occasional bug I was seeing, which can be reproduced consistently this way:
|
I'm looking into seeing why
is happening. |
@iseulde , it looks like what you are seeing is a bug in tinymce. I've recreated it in a fiddle here (http://fiddle.tinymce.com/Scgaab/1). As soon as you remove the |
@iseulde & @ephox-mogran, drive-by comment before I have to run: breaking on |
} | ||
|
||
return true; | ||
} ); | ||
} | ||
|
||
onKeyDown( event ) { | ||
const { keyCode, target } = event; |
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 missed this until now, but FYI KeyboardEvent.keyCode
has been deprecated and KeyboardEvent.key
is to be preferred.
Something to revise in the future?
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.
Probably. We've been using it in Gutenberg quite a lot, so does TinyMCE. Maybe better to discuss separately.
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.
Previously I'd avoided key
because of browser support; I don't recall if this is specifically an older IE thing, or due to the non-standard behavior described in the "Notes" at:
@ephox-mogran Yeah, the fiddle illustrates this well. Only breaks in Chrome for me, not Safari. |
Re
Culprit appears to be a fix for browser quirks:
I didn't spot any obvious override mechanism to get around this quirk fix. I played a bit with event interception and |
@mcsf the fix in normalisation of ranges for this is in code review in the TinyMCE development team. Outside of waiting for that fix for core to be merged, there's not much you can do. It's a high priority. |
@mcsf , @iseulde , how many blocking issues are there still with this PR? Are there any we have replication cases for (aside from the normalisation range bug in TinyMCE above?) Specifically, is this still happening?
|
Great to know! Can you keep us posted? Seems like we can hold off this PR till EOW, perhaps?
I don’t think I ever ran into that one.
None on my part. How about you, @iseulde? |
No problem. I'll keep you posted. Also, given that this bug is already in master, and has to do with horizontal navigation and not vertical navigation, it probably shouldn't hold up this PR which is focusing on vertical navigation. Especially, as it's an internal TinyMCE bug. Would you agree @mcsf ? |
Good point. We should also rebase and retest. |
- Split `getVerticalEdge` into two separate helpers: `computeCaretRect` and `isVerticalEdge`. - Rearrange the logic in `WritingFlow#keyDown` to make the symmetries between horizontal- and vertical-navigation handlings more apparent, as well as the lifecycle of `this.verticalRect`.
a224863
to
c519d58
Compare
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.
Aside from my issue with using textContent for non text nodes when dealing with offsets (which is already in master, so not related to this PR), it looks good to me.
You can go ahead and rebase the original approach, thanks! |
@@ -71,7 +103,9 @@ class WritingFlow extends Component { | |||
return ( | |||
<div | |||
ref={ this.bindContainer } | |||
onKeyDown={ this.onKeyDown }> | |||
onKeyDown={ this.onKeyDown } | |||
onMouseDown={ () => this.verticalRect = null } |
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.
It would have been good to assign this to a bound instance method so that it's not creating a new function reference (and thereby applying a change diff) on every render.
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.
Darn, I'm usually stringent about those.
This has been fixed as of TinyMCE 4.7.2. |
@@ -20,6 +26,8 @@ class WritingFlow extends Component { | |||
|
|||
this.onKeyDown = this.onKeyDown.bind( this ); | |||
this.bindContainer = this.bindContainer.bind( this ); | |||
|
|||
this.verticalRect = null; |
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.
Arriving at this pull request hopefully confused about what this variable is. Can we add some inline comments?
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.
Might be good to make an issue :)
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.
Comment coming.
Description
This is a rebase of #2296.
It allows the caret in a block to be vertically repositioned across focusable elements.
How Has This Been Tested?
In e.g. the demo content, place the caret in an editable container. Use the UP and DOWN arrow keys to navigate across editable areas and blocks.
Checklist: