Skip to content

spec(Selector): a closed Selector holds no option list - #9586

Open
cixzhang wants to merge 3 commits into
mainfrom
spec/selector-mount-on-open
Open

cixzhang wants to merge 3 commits into
mainfrom
spec/selector-mount-on-open

Conversation

@cixzhang

@cixzhang cixzhang commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Maintainer impact

A page with many Selectors becomes usable sooner and holds far less. Today every closed Selector keeps its whole option list mounted. Measured on a page with 20 Selectors of 249 options each (production build, popover presentation, 4× CPU slowdown, median of 7 loads), building the list on every open and removing it on close gives:

What a person waits for Today This record
Page load to first paint 1,964 ms 321 ms
Main thread blocked during load 1,975 ms 334 ms
Elements after load 30,203 202
JavaScript memory after load 52.0 MB 7.8 MB
First open, first Selector used 286 ms 397 ms
Reopen the same Selector 197 ms 250 ms
Elements after opening and closing two Selectors 30,218 218
Memory after opening two Selectors 52.3 MB 9.0 MB

Each open costs a little more (about 50 ms on a 249-option reopen in this setup). The roughly 200 ms that every open of a long list costs is separate work; this record sets no rule for it and notes it under Current-state impact.

Exact decision needed

None open. The owner decided when a Selector builds its option list.

Existing authority searched

Decision

The owner, on 2026-11-02: "I think best is we do B and we improve open time for large selector options".

Recorded as component:Selector PR3 and DEC-2:

  • While closed, a Selector renders only its trigger and its form value; no list exists on either surface (an empty inert anchor marker is allowed).
  • Every open, popover and bottom sheet alike, mounts the list; every close removes it once the close settles, after the exit animation when one plays, so the exit still plays on the list.
  • Every open, the first included, starts on the selected option (or the first enabled option when none is selected), with aria-activedescendant naming it; FR2 alignment holds.
  • Closed-trigger typeahead (FR7) and the form value (FR6) read option data, never the list.
  • aria-controls names the list only while it exists.
  • Marked "Implementation gap: not built", with a Current-state impact line.

Release class: patch [fix]. Undocumented markup is not a released contract (spec:AST-017 FR3); documented selection, keyboard, accessibility, and form behavior stays the same, so no caller must change source (FR1); a victim-free correction is [fix] with a patch bump (FR5, FR7).

Rejected alternatives

  • Build the list on the first open and keep it (A): faster reopens, but every opened Selector keeps its list in memory for the life of the page.
  • Keep today's behavior (C): every closed Selector keeps its list and the page pays for all of them at load.

Claim scope and non-goals

  • Governs only when the option list exists. Open time for long lists, MultiSelector, and the Popover default are out of scope.

Evidence and implementation relationship

  • Measurements above come from a prototype that turns on the Layer's existing lazy-mount option for the Selector popover. No implementation pull request yet.

Changed records

  • component:Selector: new PR3, DEC-2, a Current-state impact section (PR3 gap, open time for long lists), a compatibility line with the release class, and six verification rows (closed element count; first and later opens' active option and alignment; typeahead while closed; form submission; exit animation; aria-controls presence).

Writing rubric results

  • R1 narration: one sentence opened "PR3 removes"; rewritten as "A closed Selector renders no option list (PR3)". "As an open does today" rewritten as "Every open starts the same way".
  • R2 research in the record: none; the measurements live in this description only, and DEC-2 keeps a one-sentence conclusion.
  • R3 evidence naming a change: none; verification cells name tests and stories.
  • R4 rejected alternatives over one line or outside a decision: one, inside DEC-2, one line.
  • R5 pull request or issue references remaining: none in the record.
  • R6 ruled open questions still listing options: none; Open questions stays "None".

Scope

  • This PR contains specification records only.
  • It records observable behavior and preserves equivalent internal implementations.
  • Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract.
  • Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
  • It records one intentional durable decision, not a rescue for a separable tagalong.
  • The canonical knowledge owner, affected current claims, and overlapping open work are named; any ownership collision is resolved or called out for human decision.
  • Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
  • Public text and artifacts contain no internal Meta context.

Validation

  • pnpm install --frozen-lockfile: passed
  • pnpm check:knowledge: "Knowledge templates and records are aligned."
  • Prettier: clean
  • git diff --check: clean

@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 6, 2026 3:01am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 6, 2026, 3:14 AM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 2, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 2, 2026
@cixzhang

cixzhang commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 7da075c

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 2, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: the list-lifetime decision is owner-approved, but one sentence in PR3 needs fixing before this lands.

  • It says the trigger's aria-activedescendant names the active option on every open, but with search on, AR2 and spec:AST-058 FR6 put it on the search input, which holds focus.
  • Its start rule (selected, else first enabled) changes where ArrowDown, ArrowUp, searchable and disabled-first opens start today, while the compatibility line says documented keyboard behavior stays the same. Either say only that the first open starts like a later one, or record the change and reconcile it with #8868.

[Automated review]

the exit plays on the list. No list stays mounted while closed. Every open
starts the same way, the first included: the highlight starts on the
selected option, or on the first enabled option when none is selected; the
trigger's `aria-activedescendant` names it; and FR2 alignment applies.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With hasSearch, AR2 keeps the combobox relationship at the search control and spec:AST-058 FR6 puts aria-activedescendant on the element that holds focus, which is the search input; the trigger is not a combobox then. Suggest: "the element AR2 gives the combobox relationship (the trigger, or the search input when search is on) names it in aria-activedescendant".

Comment on lines +225 to +227
the exit plays on the list. No list stays mounted while closed. Every open
starts the same way, the first included: the highlight starts on the
selected option, or on the first enabled option when none is selected; the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sets a new start rule. Today ArrowDown opens on the first option and ArrowUp on the last, whatever the value; a searchable Selector opened by a press or Enter has no active option; and a disabled first option is highlighted when nothing is selected (useCombobox docs say so too). If the goal is only that the first open matches later opens, suggest: "the first open starts its highlight where a later open by the same path starts it". Otherwise this change needs a Current-state impact line and a compatibility line, and it has to agree with the FR8 that #8868 proposes.

`astryx upgrade --from 0.6.3 --apply --path .`.
- A closed Selector renders no option list (PR3). Undocumented markup is
not a released contract (`spec:AST-017` FR3), and the documented selection,
keyboard, accessibility, and form behavior stays the same, so no caller must

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While PR3 keeps its start rule, documented keyboard behavior does not stay the same: the ArrowDown, ArrowUp and search open paths change. Either narrow PR3 or name the keyboard change here and recheck the [fix] class against spec:AST-017 FR5.

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 6, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision on the Selector record changes. The /approve-spec on this pull request named the previous head, and the latest commit changed PR3, so PR3, DEC-2, the compatibility line, Current-state impact and the new verification rows need a fresh decision: an engineering owner records it by commenting /approve-spec <full head SHA> on the pull request, and the author may do so as an engineering owner.

  • Miss: the PR3, FR2 verification row (Selector.spec.md:396) still fails an implementation whose open does not start on the selected or first enabled option, while PR3 now keeps today's start (ArrowDown on the first option, ArrowUp on the last, no active option when a search list opens by a press or Enter), so reword the row to test that the first open starts where a later open by the same path starts it and that AR2's combobox element names it.

Fixing the row is a new commit, so approving the head that carries it avoids a second approval. The earlier aria-activedescendant and PR3 start-rule findings are fixed, though the description's Decision list still describes the old start rule.

[Automated review]

| FR7 | `Selector.test.tsx` press model suite | finger release over another option, finger/mouse release outside, listbox marker | The press option is selected, the listbox takes focus, a finger release outside closes the list, or the stray click selects | `audit:Selector/behavior` |
| AR5 | `Selector.activeOption.test.tsx`: the ring after keyboard input, no ring after pointer input, the ring again when keyboard input follows a pointer highlight; the active option's indicator measured against the listbox in light and dark | keyboard-active option, pointer-active option | Removing the ring, or drawing it after pointer input, fails; an indicator under 3:1 against the listbox in either mode fails (the fill alone measures 1.09:1 light and 1.15:1 dark) | `audit:Selector/accessibility` |
| PR3 | `Selector.test.tsx` mount suite and a Chromium story that counts elements | never opened, opened then closed, popover and bottom sheet | A closed Selector has any listbox, option, or search element besides its trigger, form value, and inert marker | `audit:Selector/behavior` |
| PR3, FR2 | `Selector.test.tsx` mount suite and placement tests | first open and a later open, by pointer and by keyboard, with and without a selected value | The first or a later open does not start on the selected option (or the first enabled option when none is selected), `aria-activedescendant` does not name it, or the selected row is not aligned over the trigger | `audit:Selector/behavior` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This row still fails an implementation whose open does not start on the selected option (or the first enabled one), but PR3 (:226-227) now keeps today's start: ArrowDown opens on the first option and ArrowUp on the last whatever the value, a disabled first option is highlighted when nothing is selected, and a search list opened by a press or Enter has no active option (hooks.ts, useCombobox.doc.mjs). Suggest: "The first open does not start its highlight where a later open by the same path starts it, the element AR2 gives the combobox relationship does not name it in aria-activedescendant, or FR2 alignment fails", with states "first and later opens by press, Enter, ArrowDown and ArrowUp, with and without a selected value, search on and off".

This branch was successfully deployed

1 active deployment
Preview — da6d7168 Deployed Nov 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant