Triple-click selection and Selection API issue
See original GitHub issueDescription Hello! We are currently facing a bug with triple-click selection. When we triple-click on a word, the expectation is that the whole text block gets selected, a paragraph for example. The problem here is that for 2 of the 3 browsers I tested (it happens on Chrome and Edge, not on Firefox), Slate will also select the following (or part of the following) sibling node.
You can see recordings for both behaviours, the first for Chrome (bug) and the second on Firefox (no bug).
Recording
Steps To reproduce the behavior:
- Go to https://www.slatejs.org/examples/images
- Triple-click on the first paragraph
- On Chrome the image gets selected as well, not on Firefox
Expectation The expectation would be to have the selection only on the text block that was actually targeted, and not the following one.
Environment
- Slate Version: latest
- Operating System: macOS 10.15.7
- Browser: Chrome, Edge
Context From my understanding, there is two things that feel are part of the issue.
-
The first might be because of the way
offsetsare considered within a selection. At this line: https://github.com/ianstormtaylor/slate/blob/e042ebd4a2539aeabf0996e8f1a1dad1be5860c0/packages/slate-react/src/utils/dom.ts#L110offsetis compared tochildNodes.lengthBut according to the Selection API the offset represents the number of characters selected, not the number of child nodes, so this is not comparing the same things at all. -
The second thing is, if the value of
focusOffsetis 0, it means there is nothing selected in thefocusNodeand therefore, this node should be removed from the selection range. Currently, maybe because of the first point, Slate keeps digging into thisfocusNodeto find a selectable child and include it in the range. On top of it, iffocusNodeis avoidelement, it can create more problems, as Slate is not supposed to handle the content of avoidelement.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:5
- Comments:6 (3 by maintainers)

Top Related StackOverflow Question
I’ve been investigating / trying to fix this (see comments on #4492) and wanted to record some findings:
The basic issue as I understand it is that the DOM selection API supports two kinds of selection endpoint:
but Slate selections support only text endpoints.
When a DOM selection has an element endpoint, Slate looks for a corresponding text endpoint (
ReactEditor.toSlatePoint). In most cases, this text endpoint is after the original element endpoint; so when the focus of a forward selection is an element endpoint, the Slate selection is “hanging”: its focus is at the beginning of the following block.Browsers vary on exactly what actions produce element endpoints, but they are generally actions that sensibly mean “select the whole element”:
All of these generate element endpoints (in most cases) in the browsers I’ve tested (Chrome, Firefox, Safari, all on a Mac), and in most cases the element endpoints turn into a hanging selection in Slate.
It’s good to have hanging selections! There’s no other way in Slate to represent the selection of a whole block, and there are lots of actions that sensibly work on a whole block: e.g. if you triple-click a paragraph or shift-arrow-down a paragraph and hit delete, it’s sensible for the whole paragraph to be deleted, not just the text within it. (I haven’t tested a lot of rich text editors on this but it seems pretty standard.)
But hanging selections have some bugs:
First, browsers differ in exactly how they generate selection endpoints for the actions above:
DOM.normalizeDOMPointsearches backwards for a text node when the offset is past the last child).(I feel like these differences are not really a problem—applications should do something sensible with both hanging and non-hanging selections; so inconsistencies in when hanging / non-hanging selections are generated don’t really matter. Just my opinion!)
Second, applications don’t always handle hanging selections correctly:
Range.unhangbefore checking the nodes. I think this is a bug in the example—it should properly handle both hanging and non-hanging selections. (Note that you can’t currently reproduce this in Chrome due to #4455 / #4512; it’s reproducible in Safari with a triple-click or shift-arrow-down, or in Firefox with shift-arrow-down.)@bytrangle made an attempt at fixing these bugs with #4455 / #4512. I’ve been looking into alternative fixes because @bytrangle’s fixes have caused other bugs (#4492), because they address Chrome only (the issue is more widespread), and because they fix the bugs by getting rid of hanging selections (which I think is a mistake; hanging ranges are useful).
I’ve been working on a branch that addresses these bugs by always looking for the text endpoint inside the DOM selection, never outside. This works without browser-specific fixups, but it also gets rid of hanging selections (as above I think this is a mistake). I’d rather address the bugs by fixing the examples where needed, and by understanding / fixing the behavior of hanging selections inside void nodes. What do you think? @ianstormtaylor would love your input here.
So first of all, to not mix everything below, there is different things at play here:
DOM NodeandSlate Node, which are not the same.SelectionfromSelection APIand theSlate Selection, different things as well.I will try to keep these names.
If you do a single click on you paragraph, for example between the letters
oandlof the wordbold, theSelectionobject (from Selection API) will contain this (not only, but I keep the useful part here)Here, the two
textareDOM Nodesrepresenting the wordboldand the2is after how many characters of that word you clicked. Based on that, Slate will return the first validSlate Nodein which thistextis. In our case, the full paragraph. It will then assign it to theSlate Selection.Now if you do the “click-and-drag”, your
Selectionobject (from API) will be:For Slate, it’s the same as before, it will get the first valid
Slate Nodethat contains your selection, in this case still the same paragraph, give it to theSlate Selection.And for the triple click:
Since here the Selection API returns the text of the quote as a focusNode
(DOM Node), Slate detects that this is not part of the paragraph you “triple-selected”, and will therefore add the Quote element to yourSlate Selection. And this last part will trigger all the issues we noticed, like the trigger ofisBlockActiveand the image issue.The problem here is that since the focusOffset is at 0, it means that no characters from the focusNode is actually selected, so Slate should not be giving that
DOM Nodeto theSlate Selection. To make the connection with my initial issue, this is not a problem with Firefox because unlike Chrome, it already removes this “unselected node” before returning the selection, so Slate doesn’t even know about it