History

fix(web): paint text selection over composer chips (#4139)

Yordis Prieto committed 3 months ago

Commit c7101671 parent a135f2c2 files · +93 −3Browse files

Unified · Split

2 files
  1. apps/web/src/components/ComposerPromptEditor.tsx +81 −3
  2. apps/web/src/index.css +12 −0
apps/web/src/components/ComposerPromptEditor.tsx+81−3
@@ -28,7 +28,10 @@ import { · 27 unmodified lines
2828 KEY_ENTER_COMMAND,
2929 KEY_TAB_COMMAND,
3030 COMMAND_PRIORITY_HIGH,
31+ COMMAND_PRIORITY_LOW,
3132 KEY_BACKSPACE_COMMAND,
33+ BLUR_COMMAND,
34+ FOCUS_COMMAND,
3235 $getRoot,
3336 HISTORY_MERGE_TAG,
3437 DecoratorNode,
@@ -185,7 +188,7 @@ class ComposerMentionNode extends DecoratorNode<React.ReactElement> { · 150 unmodified lines
185188
186189 override createDOM(): HTMLElement {
187190 const dom = document.createElement("span");
188− dom.className = "inline-flex align-middle leading-none";
191+ dom.className = "composer-inline-chip relative inline-flex align-middle leading-none";
189192 return dom;
190193 }
191194
@@ -323,7 +326,7 @@ class ComposerSkillNode extends DecoratorNode<React.ReactElement> { · 131 unmodified lines
323326
324327 override createDOM(): HTMLElement {
325328 const dom = document.createElement("span");
326− dom.className = "inline-flex align-middle leading-none";
329+ dom.className = "composer-inline-chip relative inline-flex align-middle leading-none";
327330 return dom;
328331 }
329332
@@ -394,7 +397,7 @@ class ComposerTerminalContextNode extends DecoratorNode<React.ReactElement> { · 64 unmodified lines
394397
395398 override createDOM(): HTMLElement {
396399 const dom = document.createElement("span");
397− dom.className = "inline-flex align-middle leading-none";
400+ dom.className = "composer-inline-chip relative inline-flex align-middle leading-none";
398401 return dom;
399402 }
400403
@@ -1167,6 +1170,80 @@ function ComposerInlineTokenBackspacePlugin() { · 766 unmodified lines
11671170 return null;
11681171 }
11691172
1173+/**
1174+ * Chips render as non-editable decorators, so the browser never paints the
1175+ * native text selection over them; without help, a selection spanning chips
1176+ * is only visible in the slivers between them. Mirror the selection onto the
1177+ * chips with a data attribute the stylesheet turns into a highlight overlay.
1178+ */
1179+function ComposerChipSelectionPlugin() {
1180+ const [editor] = useLexicalComposerContext();
1181+
1182+ useEffect(() => {
1183+ let selectedKeys = new Set<string>();
1184+ // Lexical keeps the range selection on blur without emitting an update,
1185+ // so focus is tracked separately; while blurred the native highlight is
1186+ // gone and the mirrored one has to go with it.
1187+ let hasFocus = editor.getRootElement() === document.activeElement;
1188+
1189+ const applyKeys = (nextKeys: Set<string>) => {
1190+ for (const key of selectedKeys) {
1191+ if (!nextKeys.has(key)) {
1192+ editor.getElementByKey(key)?.removeAttribute("data-composer-chip-selected");
1193+ }
1194+ }
1195+ for (const key of nextKeys) {
1196+ editor.getElementByKey(key)?.setAttribute("data-composer-chip-selected", "true");
1197+ }
1198+ selectedKeys = nextKeys;
1199+ };
1200+
1201+ const readSelectedKeys = () => {
1202+ const nextKeys = new Set<string>();
1203+ editor.getEditorState().read(() => {
1204+ const selection = $getSelection();
1205+ if ($isRangeSelection(selection) && !selection.isCollapsed()) {
1206+ for (const node of selection.getNodes()) {
1207+ if (node instanceof DecoratorNode) {
1208+ nextKeys.add(node.getKey());
1209+ }
1210+ }
1211+ }
1212+ });
1213+ return nextKeys;
1214+ };
1215+
1216+ const unregisterUpdate = editor.registerUpdateListener(() => {
1217+ applyKeys(hasFocus ? readSelectedKeys() : new Set());
1218+ });
1219+ const unregisterFocus = editor.registerCommand(
1220+ FOCUS_COMMAND,
1221+ () => {
1222+ hasFocus = true;
1223+ applyKeys(readSelectedKeys());
1224+ return false;
1225+ },
1226+ COMMAND_PRIORITY_LOW,
1227+ );
1228+ const unregisterBlur = editor.registerCommand(
1229+ BLUR_COMMAND,
1230+ () => {
1231+ hasFocus = false;
1232+ applyKeys(new Set());
1233+ return false;
1234+ },
1235+ COMMAND_PRIORITY_LOW,
1236+ );
1237+ return () => {
1238+ unregisterUpdate();
1239+ unregisterFocus();
1240+ unregisterBlur();
1241+ };
1242+ }, [editor]);
1243+
1244+ return null;
1245+}
1246+
11701247 function ComposerInlineTokenPastePlugin() {
11711248 const [editor] = useLexicalComposerContext();
11721249
@@ -1701,6 +1778,7 @@ function ComposerPromptEditorInner({ · 528 unmodified lines
17011778 <ComposerInlineTokenSelectionNormalizePlugin />
17021779 <ComposerInlineTokenBackspacePlugin />
17031780 <ComposerInlineTokenPastePlugin />
1781+ <ComposerChipSelectionPlugin />
17041782 <HistoryPlugin />
17051783 </div>
17061784 </ComposerTerminalContextActionsContext>
apps/web/src/index.css+12−0
@@ -1067,3 +1067,15 @@ label:has(> select#reasoning-effort) select { · 1066 unmodified lines
10671067 .dark .model-picker-list::-webkit-scrollbar-thumb:hover {
10681068 background: rgba(255, 255, 255, 0.15);
10691069 }
1070+
1071+/* Composer chips are non-editable decorators, so the browser skips them when
1072+ painting text selection; this overlay stands in for the native highlight. */
1073+.composer-inline-chip[data-composer-chip-selected]::after {
1074+ content: "";
1075+ position: absolute;
1076+ inset: 0;
1077+ border-radius: 6px;
1078+ background-color: Highlight;
1079+ opacity: 0.3;
1080+ pointer-events: none;
1081+}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t