|
|
# Shared extraction opportunities — rolling
|
|
|
|
|
|
updated-at: 2026-06-26
|
|
|
batches-complete: 1
|
|
|
|
|
|
Ranked by (components affected × LOC removed × divergence risk). "Should extract" vs "looks similar but
|
|
|
legitimately diverges" is called out per entry.
|
|
|
|
|
|
## EX-1 · Directional-nav index helper <!-- id: EX-001 -->
|
|
|
- verdict: **SHOULD EXTRACT** (pure math is identical; only the focus-application differs)
|
|
|
- components: select (content route), combobox (content route), dropdown-menu (content + sub-content). 4+ copies.
|
|
|
- pattern: `next/prev/Home/End` index math —
|
|
|
`loop ? (i±1+len)%len : Math.min/Math.max(i±1, …)` + the `currentIndex===-1` seed.
|
|
|
- shared bug it would kill: the `currentIndex===-1` + prev + loop branch lands `n-2` instead of last
|
|
|
(select-003, dropdown-menu-001).
|
|
|
- proposed: `nextIndex(curr: number, key: string, len: number, opts: {loop; dir; orientation}): number`
|
|
|
(pure, in `soma/keyboard` or a `list-nav.ts` next to `list-selection.ts`). Each caller keeps its own
|
|
|
focus/scroll application (real focus for menu/select-content, virtual highlight for select-virtual).
|
|
|
- est. LOC removed: ~20-35 per site (×4) − ~40 helper = net ~60-100 removed; + 1 unit-tested boundary suite.
|
|
|
- divergence note: select's *trigger* route is virtual-focus and its *content* route is real-focus — the INDEX
|
|
|
math is the same; extract only the index computation, not the focus side-effect.
|
|
|
- **B2 correction:** the `n-2` loop off-by is NOT universal. navigation-menu (and the verified menu loop math) use
|
|
|
modulo WITH a `currentIndex===-1` early-return guard, so they're correct. The off-by lives specifically in
|
|
|
Select's and DropdownMenu's content routes (no guard). The extraction still pays off (one canonical, tested
|
|
|
helper replacing ~6 copies, several already correct), but the *bug fix* is scoped to select + dropdown-menu.
|
|
|
|
|
|
## EX-2 · Dismissal trigger/anchor exclusion <!-- id: EX-002 -->
|
|
|
- verdict: **SHOULD EXTRACT** (every overlay re-implements the same bounce guard)
|
|
|
- components: select, combobox, popover, dropdown-menu (4/5) each hand-roll "ignore an interact-outside that
|
|
|
lands on the trigger (and input)" via `contains()` / `isValidEvent`, with near-identical comments about the
|
|
|
pointerdown-close-then-click-reopen bounce.
|
|
|
- proposed: a `Dismissal` option `excludeRefs: () => (HTMLElement|null)[]` (or `excludeWithin`) that the layer
|
|
|
applies before invoking the close action — removes the per-provider guard + the `isValidEvent` duplication.
|
|
|
- est. LOC removed: ~8-15 per site (×4) net positive; centralizes a subtle, repeatedly-rediscovered bug.
|
|
|
- divergence note: combobox excludes trigger+input, popover/select exclude trigger only — the option takes a
|
|
|
list, so it covers both.
|
|
|
|
|
|
## EX-3 · `selectedSet`/`expandedSet` lifted derivation (A31) — HIGHEST-VALUE EXTRACTION <!-- id: EX-003 -->
|
|
|
- verdict: **SHOULD EXTRACT — confirmed across 7 components, fixes 6 HIGH + 2 MEDIUM with one helper.**
|
|
|
- components (lead-verified, B1+B5): select-007, combobox-004 (MEDIUM) + listbox-001, grid-list-001/002,
|
|
|
tree-view-002, tree-grid-001, tag-group-001 (HIGH). Each: per-item `$derived` → `provider.isSelected(v)` /
|
|
|
`isExpanded(v)` → `opts.value.current.includes(v)` (O(N²)).
|
|
|
- proposed: a tiny shared helper `liftedSet(() => string[]): { has(v): boolean }` returning a provider-level
|
|
|
`$derived(new SvelteSet(arr))`; `isSelected`/`isExpanded` become `.has(value)` (O(1)). The providers already
|
|
|
prove the pattern works — `rovingTargetEl` is lifted exactly this way (with comments). This just extends it to
|
|
|
the selection/expansion path.
|
|
|
- est. LOC: ~2 lines per component (×7) + a 3-line helper; the value is **correctness** (removes the documented
|
|
|
"hangs at 30+" hazard from every large multi-select list/tree/grid), not LOC.
|
|
|
- divergence note: trees need `expandedSet` too (same shape, different array). Single-select select/combobox are
|
|
|
the low-risk variant but the same fix applies and is harmless.
|
|
|
|
|
|
## EX-4 · Reactive registry/cache: SvelteMap over clone-and-reassign (A33) <!-- id: EX-004 -->
|
|
|
- verdict: **SHOULD MIGRATE** (4 sites, fragile pattern)
|
|
|
- components: listbox, grid-list, tree-grid, tag-group each do `this.x = new Map(this.x)` clone-and-reassign for a
|
|
|
registry/cache. virtual-list is the positive model (SvelteMap). combobox already migrated.
|
|
|
- proposed: replace each with `SvelteMap`/`SvelteSet` — `.set`/`.delete` become O(1) + reactive without the clone,
|
|
|
and the "why clone?" foot-gun disappears. Pairs with EX-3 (same providers, same modernization sweep).
|
|
|
|
|
|
## Positive model (already extracted — the bar to match)
|
|
|
- **`list-selection.ts`** (`computeListSelection` + `resolveListItemEl`) is shared correctly between Select and
|
|
|
Combobox: pure state machine, `CSS.escape` in the resolver, no `this.handleClose()` baked in. EX-1/EX-2/EX-3
|
|
|
should follow this shape. (Note: Select's `scrollSelectedIntoView` bypasses `resolveListItemEl` and
|
|
|
reintroduces the unescaped query — select-001; fixing it = "use the helper you already have".)
|