Repository navigation
Rich text: fix iOS double tap selection, preventing focus capture under editable host - #81042
Conversation
|
Size Change: +636 B (+0.01%) Total Size: 7.81 MB 📦 View Changed
|
5aab74a to
271d0d7
Compare
aa3c28a to
663224c
Compare
🤖 PR meta 🤖📦 Bundle sizeSize Change: +368 B (0%) Total Size: 8.23 MB 📦 View Changed
⚡ PerformanceShow the resultsClient side metrics exclude the server response time. front-end-block-theme
front-end-classic-theme
media-processing
media-upload
post-editor
site-editor
|
522575a to
2c915a1
Compare
2c915a1 to
cf7f0e4
Compare
While the writing flow wrapper is the editing host, render the selected block's rich text as contentEditable="inherit" and without a tabIndex, so it is editable through the host but not a focusable editing area of its own. iOS focuses a focusable child on every tap, which fights the host for focus mid-gesture and cancels native word selection (double tap). With the child inert, focus stays on the host and native selection gestures work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
The child's inertness was tied to its block being the single selected block. When a native selection drag grew into a multi-selection, the block stopped being singly selected and the child reverted to an editing area of its own, rewriting the element under the pointer mid gesture, which breaks the in-progress drag, and reintroducing a nested editable inside the engaged editing host. Keep the child inert while its block is part of the multi-selection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
JSDOM does not implement the contentEditable getter, so it returns undefined. Comparing against an explicit opt-out let the selection sync run for the first time under JSDOM, where the selection APIs produce a degenerate record that rewrites the caret to the start. Compare against the known editable values (true and inherit) instead, which keeps the handler off in JSDOM and on for elements editable by inheritance from the editing host. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
…table While the writing flow wrapper hosts editing, the selected block's rich text is editable by inheritance (contenteditable="inherit") and no longer sets contenteditable="true" on itself. Update the places that equated being editable with that attribute value so they only treat an explicit opt-out as non-editable: the getSelectionEditableElement helper (whose miss made writing flow preventDefault every keystroke), and the arrow navigation tab candidate filter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
ddaaa9e to
544397b
Compare
While the editing host holds focus, the focus owner helper reported the label of the block around the caret, and finding that block relied on the field carrying a contenteditable attribute, which the selected block's field no longer does. The two specs asserting block labels now say what they mean: the selected block is the labelled one, and it owns focus, either focused itself or contained with the selection by the focused host. The helper goes back to trunk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
It pinned a store walk for shift+arrow extension that is no longer in the branch. The behaviour it covers is trunk's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
Handing focus to the engaged host from any focused element containing the caret was added for Firefox, which focuses the nearest focusable ancestor when a click lands on the inert field. The focus handler now keeps the block selection for that caret, and the sentinel specs pass on Firefox with the trunk condition, so the widening goes. The reclaim of focus dropped onto the default target stays: block deletion, paste and the widgets screen lose focus without it. The click's synced offset defaults to 0 like the single block sync below it, so the store never holds a field identity without an offset. Shorter comments. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
Asking the store whether the selected block is inside the last root block widened the check to every field in a closing container. Keep trunk's DOM check and let the selected block's field, editable through the host without a contenteditable attribute, count as a field too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
Any element editable through the host passed the check, so the pointer leaving a formatted word inside a field started a block selection and engaged the wrapper mid-drag. A field is one with its own contenteditable attribute, or the selected block's, editable through the host. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
The wrapper never carries a block client ID, so the client ID comparison already excludes it. Plainer comment. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
Reading isContentEditable forces a style recalculation on every block focus, so it goes last. Shorter comment. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
The tabIndex comment referred to a contentEditable="inherit" attribute that is no longer rendered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
The observer synced the field identity and caret to the store after a click in the selected block's inert field, because the field gets no focus event with the host focused. Trunk's rich text hook now syncs the selection itself while a focused editing host owns it, on selection change and on capture of the events that consume it, so the copy goes. That leaves one case: Firefox focuses the nearest focusable ancestor on a click in the inert field, so an inner group wrapper holds focus, no editing host owns the selection, and the hook stays silent. The nested split spec fails there three of three times on Firefox. Handing focus to the host from any focused element containing the caret, not only the selected block's own editable, covers it: the hook then syncs as on Chromium. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
The listener stops a field from capturing focus on a press beside it by switching it off for the duration of the press. Under the editing host the selected block's field has no contenteditable attribute and is not a focus target, so there is nothing to prevent. Trunk read the absent attribute as nothing to restore and left the field switched off after the press: rich text "should only mutate text data on input" fails. Only a field that is an editing host of its own takes the flip, which puts the rest of the listener back to trunk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
The field under the editing host has no contenteditable attribute; the contentEditable property reads "inherit" for it. The comments named an attribute value that is no longer rendered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
Focusing the editable at the collapsed selection when the host disengages was limited to the selected block's editable, so a stale caret could not focus a deselected block back after select all promoted the selection to the parent. Repeated select all inside a column now promotes and stays promoted on Chromium, Firefox and WebKit with trunk's plain focus call, so the guard goes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
On an unscrolled page the effect stays off while the caret is within the initial trigger percentage, whichever block the caret is in. The rule was limited to the last editable element, which under the editing host has no contenteditable attribute and could only be found through a field marker. Eligibility reads the selection rather than the active element, which is the host there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
The drag gesture started from an element carrying the rich text attribute key. Under the editing host the selected block's element is the field, so compare the block element's client ID with the selected one instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
@Mamaduka Hmm, probably work just moved to the |
|
Btw, I'm reverting this in #82816, so it will go up again. |
|
I noticed that this PR changes the mouse cursor over selected blocks from rich-text-cursor.mp4 |

What?
Fixes native iOS selection gestures (double tap to select a word) in paragraphs while the
editableRootediting host (#79105) is active, without losing native cross-block selection, on all devices.Why?
With the editing host active, double tap to select a word rarely worked on iOS. The selected paragraph was still a focusable editing area of its own (nested
contenteditable="true"plus a forcedtabIndex), so iOS focused it on every tap while the host code immediately moved focus back to the wrapper (a nested editable cannot retain focus once an ancestor hosts editing). That focus flip in the middle of the gesture made iOS cancel the word selection before it formed.How?
tabIndexandcontentEditableattributes from rich text element when the editable host is active, so rich tech no longer incorrectly captures focus.contenteditableattribute must be adjusted by either checkingdata-wp-rich-text-attributke-key, or the block selector, since any block opting in to the editable host is also a single rich text element.Testing Instructions
Use of AI Tools
Written with the help of Claude Code.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XrG35JQKJBX1iNof5wtfjN