From 7cd621e634d4f459e6812c677cc863df8825233d Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 24 Mar 2026 18:28:16 -0400 Subject: [PATCH] Fix Android selection handle drag breaking selection state (#274) * Fix Android selection handle drag breaking selection state The highlightSelectionPlugin creates inline decorations (wrapping text in elements) on every selection change. On Android, these DOM mutations during native selection handle drag cause the browser to lose track of the selection handles. Since these decorations are invisible when the editor is focused (via CSS), skip decoration updates for selection-only changes on Android to prevent the DOM mutations. https://claude.ai/code/session_01Qcv5Xm4QcDnzhWdTbfgQh9 * Skip selection-only decoration updates on all platforms The DOM mutations from updating inline decorations on selection changes are unnecessary since the decorations are invisible when the editor is focused via CSS. Removing the Android-only guard avoids the overhead everywhere. https://claude.ai/code/session_01Qcv5Xm4QcDnzhWdTbfgQh9 * Update decoration on blur so toolbar highlight still works The selection highlight needs to be visible when focus moves from the editor to the toolbar (e.g. for entering a link URL). Dispatch an update on blur via handleDOMEvents so the decoration matches the current selection at that point, while still skipping DOM mutations during active selection drag. https://claude.ai/code/session_01Qcv5Xm4QcDnzhWdTbfgQh9 * Trim verbose comment in highlightSelectionPlugin https://claude.ai/code/session_01Qcv5Xm4QcDnzhWdTbfgQh9 * Fix selection highlight not showing when editor is blurred The `.selected .selection-highlight` CSS rule required a `.selected` ancestor class that only exists during multiselect, so the highlight was never visible for single-block text selection (e.g. when clicking the toolbar). Remove the `.selected` ancestor requirement so the decoration is visible whenever the editor loses focus. https://claude.ai/code/session_01Qcv5Xm4QcDnzhWdTbfgQh9 --------- Co-authored-by: Claude --- app/globals.css | 2 +- components/Blocks/TextBlock/plugins.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/globals.css b/app/globals.css index 2145c5b7..24130b0a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -338,7 +338,7 @@ pre.shiki { @apply box-decoration-clone; } -.selected .selection-highlight { +.selection-highlight { background-color: Highlight; @apply py-[1.5px]; } diff --git a/components/Blocks/TextBlock/plugins.ts b/components/Blocks/TextBlock/plugins.ts index c78fb953..256ab470 100644 --- a/components/Blocks/TextBlock/plugins.ts +++ b/components/Blocks/TextBlock/plugins.ts @@ -1,11 +1,19 @@ import { Decoration, DecorationSet } from "prosemirror-view"; import { Plugin } from "prosemirror-state"; + export const highlightSelectionPlugin = new Plugin({ state: { init(_, { doc }) { return DecorationSet.empty; }, apply(tr, oldDecorations, oldState, newState) { + // Skip selection-only changes to avoid DOM mutations that break + // native selection handle dragging. On blur, we force an update + // so the highlight is visible when focus moves to the toolbar. + if (!tr.docChanged && !tr.getMeta("updateSelectionHighlight")) { + return oldDecorations; + } + let decorations = []; // Check if there's a selection @@ -20,6 +28,14 @@ export const highlightSelectionPlugin = new Plugin({ }, }, props: { + handleDOMEvents: { + blur(view) { + view.dispatch( + view.state.tr.setMeta("updateSelectionHighlight", true), + ); + return false; + }, + }, decorations(state) { return this.getState(state); }, -- 2.51.2