You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4.9 KiB
4.9 KiB
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
- 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/Endindex math —loop ? (i±1+len)%len : Math.min/Math.max(i±1, …)+ thecurrentIndex===-1seed. - shared bug it would kill: the
currentIndex===-1+ prev + loop branch landsn-2instead of last (select-003, dropdown-menu-001). - proposed:
nextIndex(curr: number, key: string, len: number, opts: {loop; dir; orientation}): number(pure, insoma/keyboardor alist-nav.tsnext tolist-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-2loop off-by is NOT universal. navigation-menu (and the verified menu loop math) use modulo WITH acurrentIndex===-1early-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
- 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
DismissaloptionexcludeRefs: () => (HTMLElement|null)[](orexcludeWithin) that the layer applies before invoking the close action — removes the per-provider guard + theisValidEventduplication. - 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
- 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/isExpandedbecome.has(value)(O(1)). The providers already prove the pattern works —rovingTargetElis 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
expandedSettoo (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)
- 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/.deletebecome 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.escapein the resolver, nothis.handleClose()baked in. EX-1/EX-2/EX-3 should follow this shape. (Note: Select'sscrollSelectedIntoViewbypassesresolveListItemEland reintroduces the unescaped query — select-001; fixing it = "use the helper you already have".)