import { useLatestRef, useRovingFocus, useUniqueId, } from "@charcuterie/logic" import type { ReactNode } from "react" import { useEffect, useRef, useState } from "react" import { toClassName } from "../toClassName.ts" import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.tsx" export type DropRailTarget = { /** * A number after the label — a lane's size, a project's open * count. Optional, and absent is a real answer: a rail of * destinations that have no size should not print a zero on every * chip. */ count?: number /** * Where the thing being moved already is. It stays **visible and * unofferable** rather than being filtered out, because a rail * whose contents change depending on what you grabbed is a rail * whose chips move under the pointer between one move and the * next — and muscle memory is the only reason this control is * faster than a menu. */ isCurrent?: boolean isDisabled?: boolean key: string label: string /** A leading glyph the app owns — an emoji, a dot, an icon. */ mark?: ReactNode } export type DropRailProps = { className?: string /** * Whether the rail is showing. The host owns this, because only * the host knows a move started — and it is the same boolean * whether the move began as a pointer drag or as a tap on a * handle. */ isOpen: boolean /** The rail's accessible name. Required — it is a listbox. */ label: string onDismiss?: () => void onPick: (targetKey: string) => void targets: readonly DropRailTarget[] } /** * The attribute the pointer hit test reads. A `data-` attribute * rather than a class, for the same reason `Board` marks its cards * with one: a class is a style and may be renamed by a consumer's * tooling, and this is a contract with a hit test. */ const TARGET_ATTRIBUTE = "data-drop-rail-target" const TARGET_SELECTOR = "[data-drop-rail-target]" /** * Every destination at once, brought to the pointer instead of the * pointer being carried to it. * * ### The measurement this exists for * * `Board` moves a card with a `Menu` on the card, and that is the * right control for a board: three or four lanes, all on screen, and * the menu opens where your hand already is. It stops being the * right control at the size the fleet's real lists reach. Docket's * Backlog draws **34 project groups down one page**, of which about * four fit on a display. Dragging a card from the first to the * thirty-fourth means holding a pointer down while the page scrolls, * and a menu of 34 items is a scrolling list inside a popup — the * same problem in a smaller box. * * A rail answers both. It pins to the top for the length of the * move, so nothing is ever off screen, and there is no travel: every * destination is the same short distance away. * * ### One control, three drivers, and that is the whole design * * WCAG 2.5.7 requires a single-pointer alternative to any dragging * movement, and `FileDropZone` next door says the same thing about * its own gesture: build the accessible path first and let the drag * be the enhancement. So the rail is **not** a drag affordance with * a fallback bolted on. It is a listbox that three things drive: * * 1. **Pointer.** While a button is held, the chip under the * pointer is active and releasing over it commits. * 2. **Tap.** Opened with no button held, it is a list you touch. * Identical markup, identical commit. * 3. **Keyboard.** Arrow keys walk it, Enter commits, Escape * abandons. `useRovingFocus` owns which chip is tabbable, so * there is no second keyboard model in this file to drift out of * step with `Listbox`. * * The rail does not know which of the three opened it, and the host * does not have to say. It reads `event.buttons` on the first * pointer event it sees: a button already down means a drag is in * flight, and nothing else needs threading through. * * ### It takes no drag-and-drop dependency, and it hit-tests chips * * The board's decision priced four libraries and took none of them, * because a published library's dependency list is inherited by * every consumer — including one with 60 KB gzipped to spend in * total. Nothing here changes that arithmetic, and this needs far * less than `Board` does: a chip is a box under the pointer, so * `elementFromPoint` answers the only geometric question there is. * No snapshot, no collision strategy, no sortable transform. * * ### It is not `Rail`, and the two must not be confused * * `Rail` is the app shell's side rail — a landmark, an `