-
Notifications
You must be signed in to change notification settings - Fork 3.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
Fix selections with non-void non-editable focus #5716
Conversation
"Non-void non-editable" refers to `contentEditable={false}` DOM nodes that are rendered by a Slate element render but which are not void elements. For instance, [the checkboxes in the checklists example][1]. [1]: https://github.com/ianstormtaylor/slate/blob/7e77a932f0489a9fff2d8a1957aa2dd9b324aa78/site/examples/check-lists.tsx#L153-L170
🦋 Changeset detectedLatest commit: f4654c3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
const focusNodeSelectable = | ||
ReactEditor.hasEditableTarget(editor, focusNode) || | ||
ReactEditor.isTargetInsideNonReadonlyVoid(editor, focusNode) | ||
const focusNodeSelectable = ReactEditor.hasTarget(editor, focusNode) |
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 not sure focusNodeSelectable
is still the best name for this variable, since the non-void non-editable nodes still aren't selectable. ReactEditor.toSlateRange
just changes the focus to a node that is selectable now. Can't think of a better name at the moment, though.
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.
Also, this PR only fixes non-selectable focus points now, but should non-selectable anchor points be fixed as well? It wouldn't be necessary for my project, but conceivably, if a non-void non-editable node contained non-interactive text, it would be easy to make a browser selection with the anchor inside that node and the focus outside 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.
Fixing non-selectable anchor points would add extra complexity in that we'd have to decide what to do when a selection starts and ends in the same non-selectable node... This PR is already an improvement on the existing behavior as-is, so maybe fixing anchors can be saved for a future 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.
Changed the variable name to focusNodeInEditor
with 98630d1.
Thanks @TyMick I will look at this one sometime this week, and try to answer your open questions as well. |
Thanks, Dylan! |
I tried adding a Jest regression test for this, but my attempts to create a selection range with Testing Library's test('fixes DOM selections that end in non-editable nodes', async () => {
const user = userEvent.setup()
const editor = withReact(createEditor())
const initialValue = [
{
type: 'check-list-item',
checked: false,
children: [{ text: 'Test item' }],
},
]
render(
<>
<div>Something outside the editor</div>
<Slate editor={editor} initialValue={initialValue}>
<Editable renderElement={CheckListItemElement} />
</Slate>
</>
)
await user.pointer([
// Mouse down in the middle of the checklist item text
{ keys: '[MouseLeft>]', target: screen.getByText('Test item'), offset: 4 },
// Move mouse outside editor
{ target: screen.getByText('Something outside the editor'), offset: 2 },
// Move mouse to the checkbox
{ target: screen.getByRole('checkbox') },
// Mouse up
{ keys: '[/MouseLeft]' },
])
await waitFor(() => expect(editor.selection).toBeTruthy())
})
function CheckListItemElement({ attributes, children, element }: any) {
const { checked } = element
return (
<div {...attributes}>
<span contentEditable={false}>
<input type="checkbox" checked={checked} />
</span>
<span suppressContentEditableWarning>{children}</span>
</div>
)
} I think this may not be testable without adding an end-to-end framework like Playwright. |
Optimize leaf node search
Rename `focusNodeSelectable` to `focusNodeIsSelectable` A more accurate name given this PR's changes.
Remove inapplicable `if` branch
Improve comment
We do actually have Playright in there, just not too many tests yet. |
Oh perfect, I should've looked around more! Taking a look at |
Nope. I have spent hours trying to
in a Playwright test, but the behavior is far too flaky to be able to reliably create the same text selection by dragging the mouse. And I can't reproduce the bug with just the keyboard. Can't think of any other way to test it. |
@TyMick I appreciate you trying. These interactions are not easy to test. |
Description
Currently, when I drag my browser selection focus into a
contentEditable={false}
DOM element that's being rendered by a non-void Slate element renderer, Slate's selection doesn't get updated. At times, this causes non-highlighted text to be deleted when pressing Backspace (first example below). At other times, this causes a Backspace press to crash the page (second example below).The fix in this PR is to set the focus point to the edge of the nearest Slate leaf when the selection focus is in a non-void, non-editable node, instead of disregarding such selections.
Issue
Fixes #5714
Example
These examples use https://www.slatejs.org/examples/check-lists, where the “non-void, non-editable” node is this
<span>
.Incorrect deletion
Before
After
Page crash
Before
After
Checks
yarn test
.yarn lint
. (Fix errors withyarn fix
.)yarn start
.)yarn changeset add
.)