From 1a9eac3583fda245a6558e98833b2eeadddf9468 Mon Sep 17 00:00:00 2001 From: lstockmann Date: Wed, 2 Sep 2026 11:38:22 +0200 Subject: [PATCH 1/2] feat(List): let the item and row text be selected and copied MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A ListItem and a table row are click targets, and React Aria sets `user-select: none` on them for the duration of a press, so their text could not be selected. The item's title and subtitle and the table's cells now opt back in, and the click that ends a mouse drag is swallowed in the capture phase — React Aria triggers the press from the click event, so selecting a heading would otherwise activate the item as well. Dragging never activates an item, whether or not it selected text: a drag that missed the text would otherwise navigate away rather than do nothing. A click that changed the text selection is swallowed regardless of distance, which covers selecting a single character and selecting by long press on touch. An ordinary click still activates the item, including one that wobbles a few pixels or lands inside an existing selection, where the browser keeps the selection alive until after the click. A drag that started on an interactive child is left alone, as is any click arriving without a pointer interaction — keyboard, screen reader, `element.click()`. The title box hugs its text exactly, so a drag would have had to hit the text pixel for pixel. The header around it is selectable as well, and the title carries padding cancelled out by the same negative margin, so a drag can start beside the text without moving it. Only a native drag creates a real text selection, and every parallel test file shares one page and one Playwright cursor, so another file's click releases the held button mid-drag. The `browser-mouse` project gives those tests a page of their own, one file at a time. Needs #3066 to work in Safari: WebKit has no unprefixed `user-select`, so without autoprefixer the rule does not apply there and the webkit browser project fails. Closes #895 Co-Authored-By: Claude Opus 5 --- AGENTS.md | 1 + packages/components/dev/vitest/vitest.d.ts | 8 + .../Items/views/GridList/GridList.tsx | 9 +- .../ListItemView.module.d.scss.ts | 2 +- .../ListItemView/ListItemView.module.scss | 18 +- .../src/components/Table/Table.module.scss | 8 + .../components/src/components/Table/Table.tsx | 4 +- .../src/lib/hooks/useIgnoreClickAfterDrag.ts | 100 +++++++++ .../mouse/ListTextSelection.browser.test.tsx | 197 ++++++++++++++++++ packages/components/src/types.d.ts | 8 + packages/components/vitest.config.ts | 19 +- packages/core/src/vitestBrowserTestConfig.ts | 71 +++++++ 12 files changed, 439 insertions(+), 6 deletions(-) create mode 100644 packages/components/src/lib/hooks/useIgnoreClickAfterDrag.ts create mode 100644 packages/components/src/tests/mouse/ListTextSelection.browser.test.tsx diff --git a/AGENTS.md b/AGENTS.md index 5619c94860..1f7b413510 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -314,6 +314,7 @@ where the error points. | A `ReactElement`-valued prop renders fine in the visual suite's **Remote** environment but kills a real remote connection with **`DataCloneError: Symbol(react.transitional.element) could not be cloned`** (the host then shows `Remote rendering failed: Timeout reached`) | The prop was generated as a **remote property**, and properties travel through `postMessage`/structured clone, which cannot carry a React element. `remote-dom-react` turns an element-valued prop into a slotted child _only_ when the element declares it as a slot (`Element.remoteSlotDefinitions.has(prop)`), and the generator declares a slot only for props typed exactly `ReactNode` or listed in `@flr-slot-props` (`dev/remote-components-generator/lib/propClassifiers.ts`) | Add `@flr-slot-props , ` to the component's JSDoc and regenerate — the prop then arrives as a slotted child and any React subtree works, including a raw `` or a Tabler icon. Verify in `apps/remote-dom-demo` over the iframe connection: the visual suite's Remote environment is **in-process** and never serializes, so it cannot see this class of bug | | `vitest run --update ` rewrites **every** baseline instead of the one file's, and `git status` shows snapshots you never touched | `--update` takes an optional value, so it swallows the positional test filter that follows it. The run then matches all files, updates all of them, and reports the full test count (354, not 2) — the only signal that anything went wrong | Put the filter **before** the flag (`vitest run --update`) or pin the flag's value (`--update=true `). Check `git status` after any `--update` and revert baselines outside your change; a stray one is indistinguishable from an intentional update once committed | | A baseline for a component you never touched changes in your PR, or a scenario starts failing right after an unrelated PR merged with `update-screenshots` | The `update-screenshots` label runs `pnpm test:visual --update` over the **whole** suite and then `git add -A` — it is not scoped to the PR's diff. Any scenario that is failing or flaky at that moment gets whatever renders then committed as its new truth | Only apply the label when the suite is otherwise green, and read the resulting commit's file list before merging. #2945 (a CodeBlock change) took a tooltip-less frame of a racy `Tooltip` scenario this way and committed it as the `firefox-linux` baseline, contradicting the three beside it and keeping the scheduled run red in both environments (#2985) | +| A browser test that drags the mouse selects only the first character — `Dragging across "…" selected "e" instead of "example.com"` — and passes when the file runs on its own | Every parallel test file shares one page and therefore **one** Playwright cursor. Another file's `click` releases the held button mid-drag, so only the first move extends the selection. Retrying inside the command does not help — the interference comes in bursts that outlast the retries | Put mouse-driven tests in `packages/components/src/tests/mouse/**`; the `browser-mouse` project runs them in a page of their own, one file at a time. Only a native drag creates a real text selection, so `Selection`/`Range` in a synthetic test is no substitute: it ignores `user-select` and would not have caught the missing WebKit prefix | | Hand-edited `MIGRATION.md` reverts on the next build, or CI fails "Check all generated code is committed" | `MIGRATION.md` is generated from `packages/codemods/src/migrations//entry.md` | Edit the catalogue entry, run `pnpm nx build codemods`, commit both | ## Where to look next diff --git a/packages/components/dev/vitest/vitest.d.ts b/packages/components/dev/vitest/vitest.d.ts index 23a7deabc5..1360327b7c 100644 --- a/packages/components/dev/vitest/vitest.d.ts +++ b/packages/components/dev/vitest/vitest.d.ts @@ -6,6 +6,14 @@ import { type Locator } from "vitest/browser"; declare module "vitest/browser" { interface BrowserCommands { setReducedMotion: (value: string) => Promise; + selectTextByDragging: ( + selector: string, + overshoot?: number, + ) => Promise; + dragMouse: ( + from: { x: number; y: number }, + to: { x: number; y: number }, + ) => Promise; } interface LocatorSelectors { getByLocator(locator: string): Locator; diff --git a/packages/components/src/components/List/components/Items/views/GridList/GridList.tsx b/packages/components/src/components/List/components/Items/views/GridList/GridList.tsx index 32c587be33..f35875dca7 100644 --- a/packages/components/src/components/List/components/Items/views/GridList/GridList.tsx +++ b/packages/components/src/components/List/components/Items/views/GridList/GridList.tsx @@ -1,18 +1,23 @@ +import { useIgnoreClickAfterDrag } from "@/lib/hooks/useIgnoreClickAfterDrag"; +import { mergeRefs } from "@react-aria/utils"; +import type { FC, ReactNode, Ref } from "react"; import * as Aria from "react-aria-components"; -import type { FC, ReactNode } from "react"; export type GridListProps = Aria.GridListProps & { tileMaxWidth: number; emptyView?: ReactNode; + ref?: Ref; }; /** @flr-generate all */ export const GridList: FC = (props) => { - const { tileMaxWidth, emptyView, ...rest } = props; + const { tileMaxWidth, emptyView, ref, ...rest } = props; + const dragRef = useIgnoreClickAfterDrag(); return ( emptyView} style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${tileMaxWidth}px, 1fr))`, diff --git a/packages/components/src/components/List/components/ListItemView/ListItemView.module.d.scss.ts b/packages/components/src/components/List/components/ListItemView/ListItemView.module.d.scss.ts index fac6e174b4..1fd8e41c9b 100644 --- a/packages/components/src/components/List/components/ListItemView/ListItemView.module.d.scss.ts +++ b/packages/components/src/components/List/components/ListItemView/ListItemView.module.d.scss.ts @@ -5,6 +5,7 @@ declare const classNames: { readonly topContent: "topContent"; readonly bottomContent: "bottomContent"; readonly heading: "heading"; + readonly title: "title"; readonly header: "header"; readonly subTitle: "subTitle"; readonly text: "text"; @@ -12,7 +13,6 @@ declare const classNames: { readonly listView: "listView"; readonly columnLayout: "columnLayout"; readonly contentWrapper: "contentWrapper"; - readonly title: "title"; readonly action: "action"; readonly avatar: "avatar"; readonly "flow--heading--heading-content": "flow--heading--heading-content"; diff --git a/packages/components/src/components/List/components/ListItemView/ListItemView.module.scss b/packages/components/src/components/List/components/ListItemView/ListItemView.module.scss index ad1fae6c9a..d6d6167052 100644 --- a/packages/components/src/components/List/components/ListItemView/ListItemView.module.scss +++ b/packages/components/src/components/List/components/ListItemView/ListItemView.module.scss @@ -39,7 +39,21 @@ overflow-wrap: anywhere; } + /* + * React Aria sets `user-select: none` on the item while pressing; a child with + * its own value does not inherit it. The header opts back in as well as the + * title, so a drag can start beside the text and not only on it — the title + * box hugs the text exactly. Its padding is cancelled out by the same negative + * margin, so the text does not move. + */ + .title { + user-select: text; + padding-inline: var(--size-px--xs); + margin-inline: calc(var(--size-px--xs) * -1); + } + .header { + user-select: text; overflow-wrap: anywhere; display: flex; gap: var(--list-item--avatar-to-title-spacing); @@ -211,7 +225,9 @@ &:has(.checkboxContainer:not(:empty)) { :global(.flow--heading--heading-content) { - inset-inline-start: calc(var(--list-item--padding) * 2 + var(--icon--size--m)); + inset-inline-start: calc( + var(--list-item--padding) * 2 + var(--icon--size--m) + ); } } diff --git a/packages/components/src/components/Table/Table.module.scss b/packages/components/src/components/Table/Table.module.scss index 9fd2d5871f..ddee9604e4 100644 --- a/packages/components/src/components/Table/Table.module.scss +++ b/packages/components/src/components/Table/Table.module.scss @@ -52,6 +52,14 @@ } } + /* + * React Aria sets `user-select: none` on the row while pressing; a child with + * its own value does not inherit it. + */ + .cell { + user-select: text; + } + .header { .column { border-block-end-width: var(--table--header-border-width); diff --git a/packages/components/src/components/Table/Table.tsx b/packages/components/src/components/Table/Table.tsx index d05e7a2e09..ec4b31a185 100644 --- a/packages/components/src/components/Table/Table.tsx +++ b/packages/components/src/components/Table/Table.tsx @@ -1,3 +1,4 @@ +import { useIgnoreClickAfterDrag } from "@/lib/hooks/useIgnoreClickAfterDrag"; import type { FC } from "react"; import * as Aria from "react-aria-components"; import clsx from "clsx"; @@ -33,6 +34,7 @@ export const Table: FC = (props) => { minWidth, ...rest } = props; + const dragRef = useIgnoreClickAfterDrag(); const rootClassName = clsx( styles.table, @@ -42,7 +44,7 @@ export const Table: FC = (props) => { ); return ( -
+
{children} diff --git a/packages/components/src/lib/hooks/useIgnoreClickAfterDrag.ts b/packages/components/src/lib/hooks/useIgnoreClickAfterDrag.ts new file mode 100644 index 0000000000..b0894984c5 --- /dev/null +++ b/packages/components/src/lib/hooks/useIgnoreClickAfterDrag.ts @@ -0,0 +1,100 @@ +import { useEffect, useRef } from "react"; + +/* + * Far enough that no ordinary click reaches it — a click is not meant to be + * pixel-perfect — and short enough to catch a deliberate drag. + */ +const dragThreshold = 10; + +const interactiveElements = + 'a[href], button, input, select, textarea, [contenteditable], [role="button"]'; + +const hasTextSelectionWithin = (element: Element) => { + const selection = element.ownerDocument.defaultView?.getSelection(); + if (!selection || selection.isCollapsed || selection.rangeCount === 0) { + return false; + } + return element.contains(selection.getRangeAt(0).commonAncestorContainer); +}; + +/** + * Returns a ref for the element containing pressable items. React Aria triggers + * an item's press from its click event, so the click that ends a mouse drag is + * swallowed in the capture phase — otherwise selecting an item's text would + * activate the item as well, and a drag that missed the text would navigate + * away instead of doing nothing. + * + * A click that changed the text selection is swallowed regardless of distance, + * which covers selecting a single character and selecting by long press on + * touch. A drag that started on an interactive child is left alone, as is any + * click that arrives without a pointer interaction — keyboard, screen reader, + * `element.click()`. + */ +export const useIgnoreClickAfterDrag = < + T extends HTMLElement = HTMLElement, +>() => { + const ref = useRef(null); + + useEffect(() => { + const element = ref.current; + if (!element) { + return; + } + + const ownerDocument = element.ownerDocument; + let pointerDownPosition: { x: number; y: number } | undefined; + let selectionChanged = false; + let isDrag = false; + + const onSelectionChange = () => { + selectionChanged = true; + }; + + const onPointerDown = (event: PointerEvent) => { + selectionChanged = false; + isDrag = false; + pointerDownPosition = + event.pointerType === "mouse" && + event.target instanceof Element && + !event.target.closest(interactiveElements) + ? { x: event.clientX, y: event.clientY } + : undefined; + }; + + const onPointerUp = (event: PointerEvent) => { + isDrag = + !!pointerDownPosition && + Math.hypot( + event.clientX - pointerDownPosition.x, + event.clientY - pointerDownPosition.y, + ) > dragThreshold; + pointerDownPosition = undefined; + }; + + const onClick = (event: MouseEvent) => { + const changedSelection = selectionChanged; + const draggedFar = isDrag; + selectionChanged = false; + isDrag = false; + + if (draggedFar || (changedSelection && hasTextSelectionWithin(element))) { + event.preventDefault(); + event.stopPropagation(); + } + }; + + ownerDocument.addEventListener("selectionchange", onSelectionChange); + element.addEventListener("pointerdown", onPointerDown, true); + element.addEventListener("pointerup", onPointerUp, true); + element.addEventListener("click", onClick, true); + + return () => { + ownerDocument.removeEventListener("selectionchange", onSelectionChange); + element.removeEventListener("pointerdown", onPointerDown, true); + element.removeEventListener("pointerup", onPointerUp, true); + element.removeEventListener("click", onClick, true); + }; + }, []); + + return ref; +}; diff --git a/packages/components/src/tests/mouse/ListTextSelection.browser.test.tsx b/packages/components/src/tests/mouse/ListTextSelection.browser.test.tsx new file mode 100644 index 0000000000..aa09972886 --- /dev/null +++ b/packages/components/src/tests/mouse/ListTextSelection.browser.test.tsx @@ -0,0 +1,197 @@ +import { + List, + ListItem, + ListItemView, + ListStaticData, + typedList, +} from "@/components/List"; +import { Heading } from "@/components/Heading"; +import { Text } from "@/components/Text"; +import { describe, expect, test, vi } from "vitest"; +import { commands, page, userEvent } from "vitest/browser"; +import { render } from "vitest-browser-react"; + +interface Data { + num: number; +} + +/* + * These tests drag the mouse to create a real text selection — nothing else + * does. Parallel test files share one page and one cursor, so they live in the + * `browser-mouse` project, which runs its files one at a time. + */ +describe("Text selection", () => { + const heading = "example.com"; + const subTitle = "Subtitle"; + const headingSelector = + ".flow--list--list-item-view--title .flow--heading--heading-text"; + const subTitleSelector = ".flow--list--list-item-view--sub-title .flow--text"; + + const TextSelectionList = (props: { onAction: () => void }) => ( + + data={[{ num: 42 }]} /> + textValue={() => heading}> + {() => ( + + {heading} + {subTitle} + + )} + + + ); + + const selectedText = () => String(window.getSelection()); + + test("selecting the heading with the mouse does not trigger the item action", async () => { + const onAction = vi.fn(); + await render(); + await expect.element(page.getByText(heading)).toBeInTheDocument(); + + await commands.selectTextByDragging(headingSelector); + + expect(selectedText()).toBe(heading); + expect(onAction).not.toHaveBeenCalled(); + }); + + // The title box hugs the text exactly, so without its padding a drag has to + // hit the text pixel for pixel. + test("starting the drag beside the text still selects it", async () => { + const onAction = vi.fn(); + await render(); + await expect.element(page.getByText(heading)).toBeInTheDocument(); + + await commands.selectTextByDragging(headingSelector, 4); + + expect(selectedText()).toBe(heading); + expect(onAction).not.toHaveBeenCalled(); + }); + + test("selecting the subtitle with the mouse does not trigger the item action", async () => { + const onAction = vi.fn(); + await render(); + await expect.element(page.getByText(subTitle)).toBeInTheDocument(); + + await commands.selectTextByDragging(subTitleSelector); + + expect(selectedText()).toBe(subTitle); + expect(onAction).not.toHaveBeenCalled(); + }); + + test("clicking the heading triggers the item action", async () => { + const onAction = vi.fn(); + await render(); + + await userEvent.click(page.getByText(heading)); + + expect(onAction).toHaveBeenCalledOnce(); + }); + + // The browser keeps a selection alive across mousedown and mouseup so it can + // be dragged, so this click still sees it – it just did not create it. + test("clicking inside an existing selection triggers the item action", async () => { + const onAction = vi.fn(); + await render(); + await expect.element(page.getByText(heading)).toBeInTheDocument(); + await commands.selectTextByDragging(headingSelector); + + await userEvent.click(page.getByText(heading)); + + expect(onAction).toHaveBeenCalledOnce(); + }); + + /* + * The row's bottom padding carries no text, so these drags select nothing — + * the empty-selection assertion is what keeps that true. + */ + describe("Dragging without selecting text", () => { + const bottomPaddingDrag = (distance: number) => { + const row = page.getByRole("row").element().getBoundingClientRect(); + const y = row.bottom - 4; + return commands.dragMouse( + { x: row.left + 20, y }, + { x: row.left + 20 + distance, y }, + ); + }; + + test("dragging across the item does not trigger the item action", async () => { + const onAction = vi.fn(); + await render(); + await expect.element(page.getByText(heading)).toBeInTheDocument(); + + await bottomPaddingDrag(60); + + expect(selectedText()).toBe(""); + expect(onAction).not.toHaveBeenCalled(); + }); + + test("a click that wobbles a few pixels still triggers the item action", async () => { + const onAction = vi.fn(); + await render(); + await expect.element(page.getByText(heading)).toBeInTheDocument(); + + await bottomPaddingDrag(4); + + expect(selectedText()).toBe(""); + expect(onAction).toHaveBeenCalledOnce(); + }); + }); + + describe("Table view", () => { + const cellSelector = ".flow--table--cell"; + + const TypedList = typedList(); + + const TableList = (props: { onAction: () => void }) => ( + + + + + Name + + + + {() => heading} + + + + + ); + + test("selecting a cell with the mouse does not trigger the row action", async () => { + const onAction = vi.fn(); + await render(); + await expect.element(page.getByText(heading)).toBeInTheDocument(); + + await commands.selectTextByDragging(cellSelector); + + expect(selectedText()).toBe(heading); + expect(onAction).not.toHaveBeenCalled(); + }); + + test("clicking a cell triggers the row action", async () => { + const onAction = vi.fn(); + await render(); + + await userEvent.click(page.getByText(heading)); + + expect(onAction).toHaveBeenCalledOnce(); + }); + }); + + test("activating the item with the keyboard triggers the item action", async () => { + const onAction = vi.fn(); + await render(); + await expect.element(page.getByText(heading)).toBeInTheDocument(); + await commands.selectTextByDragging(headingSelector); + + page.getByRole("row").element().focus(); + await userEvent.keyboard("{Enter}"); + + expect(onAction).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/components/src/types.d.ts b/packages/components/src/types.d.ts index 0d1b93a927..5455edf75a 100644 --- a/packages/components/src/types.d.ts +++ b/packages/components/src/types.d.ts @@ -32,5 +32,13 @@ declare global { declare module "vitest/browser" { interface BrowserCommands { setReducedMotion: (value: string) => Promise; + selectTextByDragging: ( + selector: string, + overshoot?: number, + ) => Promise; + dragMouse: ( + from: { x: number; y: number }, + to: { x: number; y: number }, + ) => Promise; } } diff --git a/packages/components/vitest.config.ts b/packages/components/vitest.config.ts index 252ed015b2..650b2afc8e 100644 --- a/packages/components/vitest.config.ts +++ b/packages/components/vitest.config.ts @@ -39,7 +39,24 @@ export default mergeConfig( name: "browser", setupFiles: "./dev/vitest/setupBrowser.ts", include: ["src/**/*.browser.test.{ts,tsx}"], - exclude: ["src/tests/layered/**"], + exclude: ["src/tests/layered/**", "src/tests/mouse/**"], + }, + }, + { + /* + * Tests that drive the mouse over several steps — a drag that selects + * text is the only way to create a real selection. Parallel test files + * share one page and therefore one cursor, so another file's click + * releases the button mid-drag. Its own project, running one file at a + * time, gives them a page of their own. + */ + extends: true, + test: { + ...browserTestConfig(), + name: "browser-mouse", + setupFiles: "./dev/vitest/setupBrowser.ts", + include: ["src/tests/mouse/**/*.browser.test.{ts,tsx}"], + fileParallelism: false, }, }, { diff --git a/packages/core/src/vitestBrowserTestConfig.ts b/packages/core/src/vitestBrowserTestConfig.ts index a53ad6ce69..4e7b09fe91 100644 --- a/packages/core/src/vitestBrowserTestConfig.ts +++ b/packages/core/src/vitestBrowserTestConfig.ts @@ -12,6 +12,75 @@ const setReducedMotion: BrowserCommand< }); }; +/* + * Selects an element's text by dragging the mouse across it, and resolves once + * the whole text is selected. Only a native drag selects text at all — synthetic + * events never do, and the browser extends the selection from the intermediate + * moves. + * + * Parallel test files share one page and therefore one mouse cursor, so another + * file's click can release the button mid-drag and cut the selection short. The + * moves go out as a single `steps` call, which no other action can interleave, + * and the drag is repeated until the selection covers the text. + * + * `overshoot` starts and ends the drag that many pixels outside the element, to + * cover aiming beside the text. The selection still has to come out as the + * element's text: the browser clamps it to the ends of the line. + */ +const selectTextByDragging: BrowserCommand< + [selector: string, overshoot?: number] +> = async ({ page, frame, iframe }, selector, overshoot = 0) => { + const locator = iframe.locator(selector).first(); + const box = await locator.boundingBox(); + const text = (await locator.textContent())?.trim(); + + if (!box || !text) { + throw new Error(`No visible element with text matches "${selector}"`); + } + + const selectedText = async () => + (await (await frame()).evaluate(() => String(getSelection()))).trim(); + + const y = box.y + box.height / 2; + + for (let attempt = 1; attempt <= 5; attempt++) { + await page.mouse.move(box.x + 1 - overshoot, y); + await page.mouse.down(); + await page.mouse.move(box.x + box.width - 1 + overshoot, y, { steps: 12 }); + await page.mouse.up(); + + for (let poll = 1; poll <= 10; poll++) { + if ((await selectedText()) === text) { + return; + } + await page.waitForTimeout(50); + } + } + + throw new Error( + `Dragging across "${selector}" selected "${await selectedText()}" instead of "${text}"`, + ); +}; + +/* + * Drags the mouse between two points given in the test frame's own coordinates, + * with no expectation about what the drag does. + */ +const dragMouse: BrowserCommand< + [from: { x: number; y: number }, to: { x: number; y: number }] +> = async ({ page, iframe }, from, to) => { + const frame = await iframe.owner().boundingBox(); + + if (!frame) { + throw new Error("The test frame has no layout box"); + } + + await page.mouse.move(frame.x + from.x, frame.y + from.y); + await page.mouse.down(); + await page.mouse.move(frame.x + to.x, frame.y + to.y, { steps: 12 }); + await page.mouse.up(); +}; + export const vitestBrowserTestConfig: ProjectConfig = { css: { include: /.+/, @@ -20,6 +89,8 @@ export const vitestBrowserTestConfig: ProjectConfig = { enabled: true, commands: { setReducedMotion, + selectTextByDragging, + dragMouse, }, provider: playwright({ /* From 17cc149a4e366e58d130c02d41056a8de4d79ae6 Mon Sep 17 00:00:00 2001 From: lstockmann Date: Thu, 3 Sep 2026 10:24:54 +0200 Subject: [PATCH 2/2] refactor(List): drop the text selection style comment Co-Authored-By: Claude Opus 5 --- .../List/components/ListItemView/ListItemView.module.scss | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/components/src/components/List/components/ListItemView/ListItemView.module.scss b/packages/components/src/components/List/components/ListItemView/ListItemView.module.scss index d6d6167052..5cba16f0d2 100644 --- a/packages/components/src/components/List/components/ListItemView/ListItemView.module.scss +++ b/packages/components/src/components/List/components/ListItemView/ListItemView.module.scss @@ -39,13 +39,6 @@ overflow-wrap: anywhere; } - /* - * React Aria sets `user-select: none` on the item while pressing; a child with - * its own value does not inherit it. The header opts back in as well as the - * title, so a drag can start beside the text and not only on it — the title - * box hugs the text exactly. Its padding is cancelled out by the same negative - * margin, so the text does not move. - */ .title { user-select: text; padding-inline: var(--size-px--xs);