LearnNewsExamplesServices
Frontmatter
id19670
titleA buffered value-picker list: typed options with counts, search and a cap
stateClosed
labels
enhancementaigrid
assigneesneo-gpt
createdAtOct 12, 2026, 5:42 PM
updatedAtOct 13, 2026, 5:54 AM
githubUrlhttps://github.com/neomjs/neo/issues/19670
authorneo-opus-vega
commentsCount7
parentIssue19643
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 19671 Grid header menu: trigger, floating surface and the wired value picker
closedAtOct 13, 2026, 5:54 AM
milestonev13.3

A buffered value-picker list: typed options with counts, search and a cap

Closed Backlog/active-chunk-8 enhancementaigrid
neo-opus-vega
neo-opus-vega commented on Oct 12, 2026, 5:42 PM

Context

Surface leaf of #19643 (graduated from D#19646 at eaae5352…; pre-work review 6127438609). The surface splits into two one-PR leaves: this list component, and the header menu plugin that hosts it (the sibling leaf, filed next and blocked by this one). The list is separable because it is unit-testable in isolation over an options Store and has a second consumer, src/table/header/Button.mjs, so it lives in src/list/, not under src/grid/. It consumes no API from the core leaf #19669: the host maps the core's {key, value, count} buckets into this list's options records.

The Problem

A column's distinct values can number in the thousands (bigData's numberN columns hold about 10,001 each; an id-like column one per record). list.Base renders one li per record (createItems, src/list/Base.mjs:623–645) and menu.List inherits that, so neither can host the picker; the accepted contract rejects full-cardinality rendering and one component per value. The picker must show checkbox rows with a label and a contextual count, keep every selected value visible and removable even at count 0 or when it no longer matches the search, narrow the options by a search string without touching the selection, admit at most 1,000 unselected search matches and say so honestly above that, and keep keyboard navigation logical across the buffered range.

The Architectural Reality

  • Neo.list.Buffered (src/list/Buffered.mjs) keeps the list contract (ul/li, ListModel, Navigator, Store) with pooled slots and a mandatory positive itemHeight (:142); createItemContent (:407–416) falls back to list.Base's text hook when no itemConfig is set, which is the row-content hook the contract names ("no component-per-value layer"). Focus follows logical record identity across recycling (:16–20).
  • Neo.list.Base adds a checkbox per item with useCheckBoxes (:174) and renders item text in createItemContent (:602); highlightFilterValue reads the list's own Store filter (:608), which is the options Store here, never the grid's.
  • Content-specialised list siblings: src/list/Chip.mjs, Color.mjs, Country.mjs; SCSS siblings in resources/scss/src/list/; unit siblings in test/playwright/unit/list/ (Buffered.spec.mjs).
  • The accepted contract (epic body, Cardinality and presentation decision): one list.Buffered projection, stable typed option keys, positive fixed item height, bounded viewport, multi-selection checkbox semantics, a 1,000-unselected-match cap with an honest refine message, selected values retained in the same projection, search and the cap never rewrite stored selection.
  • The options Store is owned by the host with a fresh Model (epic body: "Option records live in an owned Store with a fresh Model"); the source Store is never bound to this list.

Pre-Flight (structural fast-path): authoring src/list/ValuePicker.mjs matches the sibling pattern of src/list/Chip.mjs and src/list/Color.mjs in src/list/ (content-specialised list subclasses) on the src/list/Buffered.mjs base; sibling-file-lift applies; no novel directory choice. src/grid/header/ was considered and rejected because src/table/header/Button.mjs is the second consumer. Map-maintenance: not needed (another instance of an existing role).

The Fix

New Neo.list.ValuePicker extends Neo.list.Buffered:

  • store: the host-owned options Store; Model fields key (typed identity, the Store's keyProperty), value (the typed field value), label (display string), count (Number).
  • selection_ (reactive): the set of selected keys, a projection the host writes from the authoritative Store contribution; the list never mutates it on its own, it only reports toggles through selectionChange.
  • searchValue_ (reactive): narrows the visible options through one owned filterBy over selected keys and capped case-insensitive literal label matches on the options Store; selected options stay visible regardless of the match; the selection is untouched.
  • maxMatchingOptions_ (reactive, default 1000): the cap on unselected matching options admitted into the projection; above it the list shows the actual cohort size and a refine-search row instead of truncating silently; selected values never count against it.
  • Row content through the existing hook: checkbox (from useCheckBoxes), label, count with tabular digits; count 0 rendered as such, never hidden.
  • selectionChange event with {added, removed, selection} on a checkbox toggle, Space or Enter on the focused row; the host turns it into a filter write.
  • itemHeight default set in the subclass; Home/End and arrow keys act on logical records across the buffer, not on mounted slots.
  • SCSS resources/scss/src/list/ValuePicker.scss; unit spec test/playwright/unit/list/ValuePicker.spec.mjs (sibling Buffered.spec.mjs).

Contract Ledger

Target surface Source of authority Delivered behavior Fallback Docs Evidence
Neo.list.ValuePicker src/list/Buffered.mjs Fixed-height plain-vdom option pool, visual checkbox, label and contextual count; host owns options Store none (new class) class JSDoc; host leaf owns the Grids guide unit + component specs
selection_ D#19646: controls project authoritative Store state Host-written unique keys; external writes repaint without an event; no self-selecting model empty array config JSDoc external projection, pointer and key controls
searchValue_ D#19646 option-search contract Case-insensitive literal label substring through the owned filterBy; selected keys exempt; safe text-node highlighting; selection unchanged empty string admits capped unselected options plus selected config JSDoc literal metacharacters, selected nonmatches and clear controls
maxMatchingOptions_ D#19646 cardinality decision Exactly N unselected matches plus every selected key; actual matching cohort in a non-record refine message 1000 config JSDoc above/at/below cap, selected zero-count and footer extent
Owned options predicate collection.Filter contribution identity and Store.filters property: 'label', ownerRole: 'value-picker:<id>'; selected union first N matching unselected keys. Authoritative filter replacement reinstates this predicate through the next filter refresh, preserving host filters. Replacement/destroy releases only the exact owned instance absent predicate is rebuilt from the current unfiltered options class/refresh/release JSDoc filter replacement, source replacement and teardown controls
Options record edits Store.recordChange and Buffered's mounted-row writer Count-only changes repaint mounted rows without admission scans; other edits coalesce per task, cancelled on source replacement/destroy unknown change shape schedules one scan callback JSDoc 500-count zero-scan and 500-label one-scan burst; queued-lifetime controls
selectionChange D#19646 host authority {added, removed, selection} toggle intent for pointer, Space and Enter; host must echo authoritative state no change on external projection writes event JSDoc pointer/key intent and echo controls
Options fields key, value, label, count D#19646 host-owned fresh Model/Store Typed opaque key identity; label/count are display facts, never selection identity or recomputed source counts missing label uses String(value) class JSDoc equal-label distinct keys, explicit zero, arrivals and literal text
Navigator.subscribe.workerKeys actual Main key handler plus worker KeyNavigation Opt-in exact native key names cancel browser default and skip mounted-DOM handling, while worker delivery continues omitted: existing Navigator keys subscription JSDoc actual Home/End and cross-pool Arrow input; no synthetic Enter click
Navigator.subscribe.scrollIntoView existing Navigator focus effects Opt-in false suppresses both direct and focus-handler scroll calls when the worker owns logical scrolling omitted/true: existing smooth scroll subscription JSDoc zero picker scrollIntoView calls; existing menu controls
neonavigate.activeRecordId active DOM item's captured data-record-id Navigation carries the key captured at emission; a record change on the same physical active slot also fires. This applies to all Buffered subscribers with record data; picker rejects stale slot/key pairs no record data: existing element-identity firing Navigator event/source JSDoc recycled-identity unit control and real keyboard path
List accessibility W3C APG listbox pattern Multi-select listbox; option rows use aria-selected only. Spacers/footer are presentational; overflow footer is exposed by aria-describedby and removed below cap generic Values label, host may supply root labeling class/root JSDoc unit roles/state and browser role/description checks

Design read (for the operator, before the PR)

A row reads left to right: checkbox, the value's label, then the count right-aligned in tabular digits and a muted colour. Selected rows look like any other row with a checked box; a selected value with count 0 keeps its row and shows 0. When the search narrows the options, unselected non-matches disappear and selected values stay in place. Above the cap, the last row is a non-selectable line in the muted colour: "1,240 values match, narrow the search". No Apply button: every toggle is immediate. Keyboard: arrows move, Space toggles, Home/End jump to the first and last logical option.

Acceptance Criteria

  • AC1: Neo.list.ValuePicker renders an options Store of 10,001 records with a bounded number of mounted rows (the Buffered pool), each row showing checkbox, label and count through the row-content hook; no component instance per option.
  • AC2: selection is a projection: setting it externally updates the checked rows without firing selectionChange; toggling a row fires selectionChange with {added, removed, selection} and leaves the selection config unchanged until the host writes it back.
  • AC3: searchValue narrows unselected options by case-insensitive substring on label, keeps every selected option visible, and never changes selection; clearing the search restores the admitted options.
  • AC4: With more than maxMatchingOptions unselected matches, the list admits exactly that many, shows the actual cohort size in a refine row, and keeps all selected options; at or under the cap no refine row renders.
  • AC5: A selected option whose count drops to 0, or whose label no longer matches the search, stays rendered and removable; a newly arriving option renders unchecked with its count.
  • AC6: Space and Enter toggle the focused row; ArrowDown, ArrowUp, Home and End move focus by logical record across the buffer boundary (the witness asserts the focused record id, not the slot).
  • AC7: The unit spec test/playwright/unit/list/ValuePicker.spec.mjs covers AC1–AC6 and runs green in the unit tier; itemHeight has a subclass default.
  • AC8: No change in src/grid, src/collection or src/data; the options Store is the only Store the list touches.

Out of Scope

Counting and filter ownership (#19669); the floating surface, the trigger, the search TextField UI, the Clear action and the wiring to the Store contribution (the host leaf); case-sensitive or regex search; an exclusion-set mode; the bigData journey and latency witness (host leaf).

Avoided Traps

  • Rendering through menu.List: full-cardinality li rendering, and its dismissal and selection behaviour do not transfer by swapping modules.
  • One component per value via itemConfig: the contract names the row-content hook.
  • Label identity: options are keyed by the typed key; equal labels never merge.
  • Applying the cap to selected values, or letting the search or the cap rewrite the selection.
  • Binding the grid's Store to the list: the list only ever sees the host-owned options Store.

Related

Parent #19643 · core leaf #19669 (Euclid; not a dependency of this list) · host leaf (filed next, blocked by this one) · source D#19646 · siblings src/list/Chip.mjs, test/playwright/unit/list/Buffered.spec.mjs.

Decision Record: Not needed, as classified by D#19646. Decision Record impact: none. Structure-map: N/A — Engine src/list/, fast path above.

Sweeps (2026-10-12 15:38Z): live latest 20 open issues (#19669 is the core leaf; no list component equivalent); A2A last 30 in all read-states (Euclid's #19669 claim is the complementary core; no surface claim); MC rationale sweep on the problem's nouns (6 results, no prior decision beyond D#19646); own-assignment sweep (4 open, none overlapping).

Origin Session ID: a9e3a190-156a-4fdb-9ea0-a554fa796d87 Retrieval Hint: "list.ValuePicker buffered checkbox options counts search cap" · "D19646 Buffered projection 1000 unselected matches"

tobiu referenced in commit f94f1b5 - "feat(list): buffered value picker with counts and logical keys (#19670) (#19709) on Oct 13, 2026, 5:54 AM
tobiu closed this issue on Oct 13, 2026, 5:54 AM