[lexical][lexical-playground] Feature: Improve focus management in Toolbar and adds SKIP_SELECTION_FOCUS_TAG#7804
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Hi @KaiPrince! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
| } | ||
|
|
||
| .toolbar .toolbar-item:focus { | ||
| outline: 2px solid rgb(60, 132, 244); |
There was a problem hiding this comment.
I couldn't find a standardized focus colour, so I copied this colour from
| if (key === 'Escape') { | ||
| onClose(); | ||
| } |
There was a problem hiding this comment.
Moved this above the (!items) check, because some dropdowns, e.g. the colour picker don't have any buttons registered, but should still be closable.
| highlightedItem.current.focus(); | ||
| } else if (!items) { | ||
| if (dropDownRef && dropDownRef.current) { | ||
| focusNearestDescendant(dropDownRef.current); |
There was a problem hiding this comment.
For colour pickers, etc. we want to focus the first field.
There was a problem hiding this comment.
Created some utility functions for later reuse. Alternatively, could have just added a dependency to a library like tabbable
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
etrepum
left a comment
There was a problem hiding this comment.
It looks like this is failing a lot of tests
|
Hmm, yes.. I'll look into it. I'm betting it's just the tests themselves that need to be updated. |
|
Nope I take that back it's not the tests it's me. |
KaiPrince
left a comment
There was a problem hiding this comment.
Assuming I haven't missed anything significant, I'll look into adding tests next.
| useEffect(() => { | ||
| // Check if the dropdown is actually active | ||
| if (innerDivRef.current !== null && onChange) { | ||
| onChange(selfColor.hex, skipAddingToHistoryStack); | ||
| setInputColor(selfColor.hex); | ||
| } | ||
| }, [selfColor, onChange]); |
There was a problem hiding this comment.
I removed this because it seems to go against React guidelines on useEffect: https://react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers. I've replaced it with individual calls in each event handler.
As a side effect, this also fixed the odd behaviour that selecting text and then opening the color picker splits the text node even if you don't change the text color.
| useEffect(() => { | ||
| if (color === undefined) { | ||
| return; | ||
| } | ||
| const newColor = transformColor('hex', color); | ||
| setSelfColor(newColor); | ||
| setInputColor(newColor.hex); | ||
| }, [color]); |
There was a problem hiding this comment.
Similarly, this seems to go against React's recommendations: https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
|
npm run ci-check is failing: https://github.com/facebook/lexical/actions/runs/18002165768/job/51264652086?pr=7804 |
|
Huh. I'm not seeing that issue locally. Let me investigate |
| @@ -253,7 +276,10 @@ export default function DropDown({ | |||
|
|
|||
| {showDropDown && | |||
| createPortal( | |||
| <DropDownItems dropDownRef={dropDownRef} onClose={handleClose}> | |||
| <DropDownItems | |||
| dropDownRef={dropDownRef as React.RefObject<HTMLDivElement>} | |||
There was a problem hiding this comment.
You shouldn't need to add a cast, casts are unsafe. The correct place to fix this are the types themselves. This is a direct consequence of the Ref vs. RefObject thing you keep trying to fix but was actually correct before and now it isn't.
type Ref<T> = RefCallback<T> | RefObject<T | null> | null;
The definition of RefCallback contains the internal implementation details that you keep running away from, but using Ref and RefCallback as-is are perfectly correct and is not something that needs to be avoided or "fixed".
There was a problem hiding this comment.
Hmm yes I'm not pleased with using a cast here either. Based on observation the casted type seemed to be correct, but now that you pasted the type definition, it seems like I have the wrong version of react installed because I get a different type def:
Could you link me the source for the definition you pasted?
Oh and just to clarify I'm not trying to run away from RefCallback, I'm just trying to best utilize the type system by narrowing the acceptable types to what we actually would expect. Having a stricter interface keeps the implementation simple since we don't have to add behaviour to handle unexpected inputs. e.g. DropDownItems is an internal component of Dropdown, which we know only uses RefObject and never RefCallback, so by being more strict on the input type we don't need to add a check for handling RefCallback, leading to simpler code.
There was a problem hiding this comment.
I'll try resetting my node_modules and running npm ci. In the meantime, adjusting the prop type to match the version you pasted fixes it in CI: KaiPrince@3cd1c6c
If you still feel strongly that I should revert it back to just Ref, let me know.
etrepum
left a comment
There was a problem hiding this comment.
I think this looks good, great work and nice job cleaning up the inconsistencies with React best practices as well. Will merge assuming that the e2e suite is still passing.
Description
In this PR, I am addressing the issue of focus being moved to the editor when a toolbar button is activated with the keyboard, as reported in #6006.
I have made the following behavioural changes:
This PR partially addresses #6006.
This PR does not address the behaviour of modals such as the insert image button, or the trap created by the tab intent plugin.
SKIP_SELECTION_FOCUS_TAG
Normally when an editor.update reconciles its selection to the DOM, the focus is moved to the rootElement if it does not already have focus. In order to ensure that the DOM selection changes but the focus remains on the button, a new
SKIP_SELECTION_FOCUS_TAGhas been added which is more fine-grained thanSKIP_DOM_SELECTIONwhich avoids changing the DOM selection or focus.Test plan
Before
Toolbar button:
Dropdown button:
After
Toolbar button:
Dropdown button: