Skip to content

feat(Tokenizer): forward BaseTypeahead's menu, onOpenChange and open handle - #10624

Open
cixzhang wants to merge 27 commits into
mainfrom
feat/tokenizer-menu
Open

cixzhang wants to merge 27 commits into
mainfrom
feat/tokenizer-menu

Conversation

@cixzhang

@cixzhang cixzhang commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #10544 → #10624 → #10625 → #10847
Merge group: #10544, #10624, #10625 (lands together; none merges alone)

Who this helps

People using Tokenizer fields (for example a filter bar whose tokens open an editor). Tokenizer can now use BaseTypeahead's menu (#10544): a token's own button can open a dialog-role menu on the field's anchored layer, and Escape returns focus to that token.

What changed

  • Forwarding: in its inline presentation, Tokenizer forwards menu and onOpenChange to BaseTypeahead; TokenizerHandle gains open() and close().
  • Token focus: Tokenizer keeps focus where a press, or the menu's focus return, put it, instead of redirecting it to the input, so Escape returns focus to the token whose button opened the dialog.
  • Sheet presentation: keeps its own sheet; menu and onOpenChange join its development warning, and the handle's open()/close() do nothing there.
  • Record: component:Tokenizer FR28 and its public-contract line; docs for the new props.

Changeset: [feat] patch for Core.

Verification

  • Tests: BaseTypeahead.menu.test.tsx "Tokenizer forwards the menu, the handle and onOpenChange" and "Escape returns focus to a Tokenizer token whose button opened the dialog" (mouse and keyboard; the second fails without the token-focus change). Tokenizer and PowerSearch suites pass (766).
  • Other checks: knowledge, changesets, and the prepush typecheck.

Writing rubric results

Covers component:Tokenizer FR28 and the public-contract line.

R1 narration: none; FR28 states what Tokenizer does.
R2 research: none in the record.
R3 evidence: the verification cell names tests, not pull requests.
R4 rejected alternatives: none recorded.
R5 pull request or issue references: none added.
R6 open questions: none touched.

… an open handle

BaseTypeahead takes `menu`, which chooses what its anchored menu shows while
Astryx keeps the layer (position, top layer, outside press, Escape, focus
return, ARIA). The listbox role may put information around the result list;
anything a person acts on stays a result, and a control outside the list
warns in development. The dialog role shows content that takes focus in a
dialog named by `label`; closing it returns focus to the element focused when
it opened, or to the input, and it does not close on an empty query or an
input blur. `handleRef` exposes open() and close(); onOpenChange reports
changes; there is no controlled open state. open() runs after the commit it
is called in, and a pick whose onChange calls open() keeps the menu open, so a
role switch keeps the same layer with no close in between. Typeahead and
Tokenizer forward the menu (Tokenizer in its inline presentation, with
onOpenChange and open/close on its handle). Records: BaseTypeahead FR13-FR16,
AR6, ORD3 and DEC-1 (row 49 A), Typeahead FR11, Tokenizer FR28. Adds the
BaseTypeaheadMenu CLI block (the docsite example), a Menu story and a
Chromium spec.
…n() opens it

Addresses review on the menu config:
- An outside press closes a dialog-role menu: the outside-press watch finds
  the layer from the element the input controls, in its own root, instead of
  the list the dialog replaces.
- Only the handle opens a dialog-role menu; a search, focus load, focus or
  ArrowDown leaves it closed, so typing stays in the field.
- The dialog role drives usePopover (role, dialogLabel, hasAutoFocus): the
  popover is the named, modal dialog, takes initial focus and restores it. The
  shared focus trap gains a restore fallback, used when the element focused at
  open is gone or was the page, and BaseTypeahead passes its input.
- A pick that calls open() and stays a listbox returns focus to the input and
  loads the focus entries, or closes when there are none.
- `hasNoResults` is now `isEmptySearch` (spec:AST-056 vocabulary); the
  outside-list warning runs in development only.
- Records: contract-at-a-glance and compatibility say the change is additive;
  FR1, FR4 and AR1 are qualified by role; FR14 and FR16 state the open and
  pick rules; anatomy names Menu content and Menu dialog; verification rows
  name the new tests and the Chromium outside-press case.
… CI fixes

- Closing a dialog-role menu that a pick opened (Escape, close(), an outside
  press) returns focus to the input without opening the list again, decided
  in the close callback, as a pick's own focus return already does. Tests
  cover Escape, close() and an outside press in a hasEntriesOnFocus field.
- The outside-list warning gates on a declared build flag, as
  utils/devWarning.ts does, so processEnvTypes passes.
- Tokenizer.spec: the Public contract row is main's, with the FR28 sentence
  appended; FR28 gains its Status cell.
- BaseTypeahead.spec: AR6 says aria-controls names the layer that holds the
  dialog; FR14 names what the dialog render receives; FR15 says a focus
  return does not open the list.
- The BaseTypeaheadMenu block joins the ShadCN route lock and the CLI search
  goldens.
… open(); Tokenizer and CLI block move out

- The dialog's close moves focus through the focus trap's new internal
  restoreFocusTo hook, so BaseTypeahead knows which focus is the return
  and keeps the list closed, however late close() runs (after an await).
- open() records the element focused when it is called, and the menu
  returns focus there (usePopover's internal getOpener), so a click's own
  handlers that move focus first no longer take the opener.
- To stay within the size limit, Tokenizer's forwarding (menu,
  onOpenChange, the handle's open and close, FR28) and the BaseTypeaheadMenu
  CLI block, with its route-lock and golden entries, move to follow-up pull
  requests. The Menu story keeps the example.
- Test: close() after an await.
…handle

Tokenizer's inline presentation forwards `menu` and `onOpenChange` to
BaseTypeahead, and TokenizerHandle gains `open()` and `close()`. A token
whose button calls `open()` with a dialog-role menu opens the dialog, and
Escape returns focus to that token: Tokenizer keeps focus where a press,
or the menu's focus return, put it, instead of redirecting to the input.
The sheet presentation keeps its own sheet; `menu` and `onOpenChange` join
its development warning. Record: Tokenizer FR28, the public-contract line.
Part of the BaseTypeahead menu stack.
@vercel

vercel Bot commented Nov 8, 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 9, 2026 2:38pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 8, 2026
@astracat-bot

astracat-bot Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 9, 2026, 3:56 PM UTC

@cixzhang

cixzhang commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 91f2921

@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.

Changes requested. The menu and onOpenChange forwarding and the new open()/close() handle match component:Tokenizer FR28, but both new focus flags need rework before this lands.

  • A press that starts on a token's control and is released outside the field leaves isPressingRef set (Tokenizer.tsx:1541–1549 clears it only on a pointerup inside the field), so the next Tab into the field stops on the first token's Remove button instead of the input; read the shared interaction-modality tracker instead, or end the press wherever it is released, and drop the wrapper's own pointer handlers.
  • handleMenuOpenChange (Tokenizer.tsx:1186–1205) guesses the menu's focus return with document focusin/keydown/pointerdown listeners that outlive an unmount, while the shared focus trap's restoreFocusTo, which BaseTypeahead already uses, marks exactly that focus, so have BaseTypeahead report the return to Tokenizer through that hook and delete the listeners and the flag.

A TokenizerBottomSheet.test.tsx case for the sheet's menu/onOpenChange warning and its no-op open() would also close the test gap FR28 records.

[Automated review]

The owner decided TextInput exposes no ref to its control box for
anchoring a Popover: that route handles none of the accessibility a
typeahead needs. A custom typeahead uses BaseTypeahead with anchorRef and
the menu config.
Tokenizer.spec.md: main's requirement table (FR16) with FR28 added.

@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.

This review waits on an engineering owner's spec decision, and two focus fixes are needed.

component:Tokenizer FR28 is new, and the earlier /approve-spec named the previous commit, whose spec content the base merges have since changed, so it no longer covers this head. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request (the author may, as an engineering owner) or asks for changes to FR28 and its four API rows; FR28 also relies on component:BaseTypeahead FR13–FR16, which land with #10544.

Blocking, whatever the spec decision:

  • A press that starts on a token's control and is released outside the field leaves isPressingRef set (Tokenizer.tsx:1544–1552 clears it only on a pointerup inside the field), so the next Tab into the field stops on the first token's Remove button instead of the input; derive this from the shared useInteractionModalityTracking() or end the press wherever it is released, and drop the wrapper's pointer handlers.
  • handleMenuOpenChange (Tokenizer.tsx:1186–1205) guesses the menu's focus return by arming a flag on every close and clearing it from three document listeners that outlive an unmount; have BaseTypeahead report the return it already performs in restoreFocusTo through a package-internal option, and remove the listeners.

The fixed head needs its own /approve-spec.

[Automated review]

@cixzhang

cixzhang commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 863f6fa

@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.

Changes requested: component:Tokenizer FR28 and its four API rows are approved for this head, so the forwarding, the handle and the token-focus direction are settled, but three focus defects in Tokenizer.tsx block.

  • A press on a token's control that is released outside the field leaves isPressingRef set (Tokenizer.tsx:930, :1544–1552), so the next Tab into the field stops on the first token's Remove button instead of the input; keep focus only when the shared modality tracker (useInteractionModalityTracking()) reads 'pointer', or at least clear the flag when focus leaves the field.
  • handleMenuOpenChange (Tokenizer.tsx:1186–1205) arms the focus-return flag on every menu close and clears it with three document listeners, so after a result-list close a focus that reaches a token with no key or press stays there and an unmount leaves the listeners registered until the next focus, key or press; have BaseTypeahead report its focus return (it already passes the focus trap's restoreFocusTo) through a package-internal option and drop the listeners.
  • Miss: in Safari a click does not focus the token's button, so open() never records the token as the opener and Escape returns focus to the input instead of the token that FR28 and the Changeset promise; record the pressed token control as the opener and add a test that clicks without focusing.

Non-blocking: FR28's sheet sentence (the warning for menu and onOpenChange, and open()/close() ignored) has no test.

A fixed head needs a fresh /approve-spec, and this pull request lands only together with #10544 and #10625.

[Automated review]

…s own return

- Focus a pointer puts on a token's control stays there because the shared
  interaction-modality tracker reads "pointer", not a press flag that a
  press released outside the field left set. Test: a press on a token
  released outside the field still lets the next Tab land on the input
  (fails on the previous head).
- BaseTypeahead marks its dialog menu's focus return through a
  package-internal __menuFocusReturnRef, set by the focus trap's
  restoreFocusTo, so Tokenizer keeps that focus on the token without the
  three document listeners it armed on every close.

@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, and two fixes are needed whatever that decision is.

  • component:Tokenizer FR28, its public-contract line and its handle paragraph are new, and the earlier /approve-spec named the previous commit: an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or says what to change.
  • BaseTypeahead's new __menuFocusReturnRef is tagged internal but reachable through BaseTypeaheadProps, and no record decides it, so an owner needs to accept it on the public props or ask for it to move behind a non-exported boundary.

Blocking findings:

  • Miss: in Safari, where a click or tap does not focus the token's button, open() records the page or the input as the opener, so Escape returns focus to the input instead of the token that FR28 and the Changeset promise; record the pressed token control as the opener and add a test that clicks without focusing.
  • Miss: FR28, the Changeset and the comment at Tokenizer.tsx:928–929 say focus a press puts on a token's control stays there instead of moving to the input, but handleWrapperClick still moves it to the input when the click completes, so reword them to what holds (focus stays until the click runs, so open() returns focus to that token on Escape).

Fixed since the last review: Tab entry after a press released outside the field lands on the input again, and the menu's focus return now comes from BaseTypeahead instead of document listeners. Non-blocking: FR28's sheet sentence has no test. This pull request lands together with #10544 and #10625.

[Automated review]

Comment thread .changeset/tokenizer-menu.md Resolved
@cixzhang

cixzhang commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 944e86f

@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.

Changes requested: the spec is approved, and two focus fixes remain.

An engineering owner approved component:Tokenizer FR28 for this head, so the new menu, onOpenChange and handle open()/close() have their governing record, and BaseTypeahead's internal __menuFocusReturnRef was accepted with it.

Blocking findings:

  • Miss: in Safari, where a click or tap does not focus the token's button, open() records the page as the opener, so Escape returns focus to the search input instead of the token that FR28 and the Changeset promise; record the pressed token control as the opener and add a test that clicks without focusing.
  • Miss: FR28, the Changeset and the comment at Tokenizer.tsx:928–929 say focus a press puts on a token's control stays there instead of moving to the input, but handleWrapperClick still moves it to the input when the click completes; make the click handler leave focus on a token's own control, or reword those three to what holds (a reworded FR28 needs a new /approve-spec).

Non-blocking: FR28's sheet sentence has no test. This pull request lands together with #10544 and #10625.

[Automated review]

…ntrol keeps focus

- In Safari a click does not focus a button, so open() recorded the page
  as the opener and Escape returned focus to the input. Tokenizer now
  records the control a press lands on inside a custom token, and
  BaseTypeahead's open() reads it (internal __pressedOpenerRef) before the
  focused element.
- A click on a custom token's own control no longer moves focus to the
  input, as FR28 and the changeset say.

Tests: Escape returns to a token whose click did not focus it; a click on
a custom token's control leaves focus on it (both fail on the previous
head).

@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 spec decision, and two fixes are needed whatever that decision is.

component:Tokenizer FR28, its public-contract line and handle paragraph are unchanged, but the new commit ends the earlier approval, and BaseTypeahead's public props now also reach the internal __pressedOpenerRef, which no record decides. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or says what to change.

Blocking findings:

  • In Safari, the blur that a press on a token's button causes clears the pressed control (Tokenizer.tsx:1254) before the click calls open(), so Escape still returns focus to the search input whenever focus was in the field, the usual state after typing or picking a value; keep the pressed control through that blur and add a test that focuses the input before the press.
  • findTokenControl re-declares a focusable selector (Tokenizer.tsx:954–956) instead of using the canonical FOCUSABLE_SELECTOR from hooks/focusableSelector.ts, and the two already differ (a contenteditable or summary control inside a token is missed); import it, or add the click-target variant beside it and import that.

The click fix from the last review holds: a click on a custom token's control now keeps focus there. Non-blocking: in Safari that click leaves focus on the page, and FR28's sheet sentence has no test. This pull request lands together with #10544 and #10625.

[Automated review]

@cixzhang

cixzhang commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 9716e63

BaseTypeaheadFocusContext now carries Typeahead's inputReturnTarget beside
Tokenizer's two refs; the restore point uses the stand-in and still sets
menuFocusReturn around the focus.

@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 spec decision for this head: component:Tokenizer FR28, with its public-contract line, handle paragraph and the four API rows it adds (Tokenizer menu and onOpenChange, TokenizerHandle.open() and close()), was approved for the previous commit, and this merge of #10544's Typeahead fix ends that approval although the pull request's own changes are the same. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner. No blocking issue remains, and this pull request lands only together with #10544 and #10625, because FR28 relies on BaseTypeahead's menu from #10544.

Non-blocking:

  • Under Safari's click model, a click on a custom token's control that opens nothing leaves focus on the page instead of the input (Tokenizer.tsx:1506–1509).
  • FR28's sheet sentence (the development warning for menu and onOpenChange; open() and close() ignored) has no test.

[Automated review]

@cixzhang

cixzhang commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 3a17bfd

@cixzhang
cixzhang changed the base branch from feat/typeahead-menu to main November 9, 2026 07:48

@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.

Approved: an engineering owner approved component:Tokenizer FR28 for this head, with its public-contract line, handle paragraph and the four API rows it adds (Tokenizer menu and onOpenChange, TokenizerHandle.open() and close()), and no blocking issue remains after this merge of #10544's restore-point fix. This pull request lands only together with #10544 and #10625, because FR28 relies on BaseTypeahead's menu from #10544.

Non-blocking:

  • Under Safari's click model, a click on a custom token's control that opens nothing leaves focus on the page instead of the input (Tokenizer.tsx:1506–1509).
  • FR28's sheet sentence (the development warning for menu and onOpenChange; open() and close() ignored) has no test.
  • The merged BaseTypeaheadFocusContext.ts header grows to 16 lines, with one long sentence for both refs; a shorter header is suggested inline.

[Automated review]

Comment on lines +15 to +19
* Tokenizer provides two refs: `menuFocusReturn` is `true` while a
* dialog-role menu's close returns focus, so the field keeps that focus on
* the token; and `pressedOpener` holds the token control a click activates,
* which `open()` records as the menu's opener even when the click did not
* focus it (a click does not focus a button in Safari).

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.

Non-blocking (INV29): with the merge this header grows to 16 lines, past the limit of about 12, and these lines put both refs into one 56-word sentence. A shorter header keeps the reasons:

/**
 * @file BaseTypeaheadFocusContext.ts
 * @input Uses createSharedContext
 * @output Exports BaseTypeaheadFocusContext (internal; not re-exported)
 * @position Internal to Typeahead; provided by Typeahead and Tokenizer, read
 *   by BaseTypeahead
 *
 * A dialog menu's close returns focus to `inputReturnTarget` in place of a
 * hidden input. `menuFocusReturn` is true while that close returns focus.
 * `pressedOpener` is the control a click activates. Safari does not focus a
 * clicked button.
 */

@cixzhang cixzhang closed this Nov 9, 2026
@cixzhang cixzhang reopened this Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 9f908c0

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
PowerSearch 288.6 kB (+1.3 kB) · 24.1 kB CSS (0) — — · — · —
Tokenizer · Storybook 92.7 kB (+1.2 kB) · 18.8 kB CSS (0) 2,331 (+92) ✓ · ✓ · ✓
Typeahead 80.9 kB (+1.1 kB) · 12.4 kB CSS (0) — ✓ · ✓ · ✓
48 more components
Component Size LoC Visual · A11y · RTL
BaseTypeahead 72.2 kB (+953 B) · 10.9 kB CSS (0) — ✓ · ✓ · ✓
Selector 100.1 kB (+191 B) · 17.4 kB CSS (0) — — · — · —
MultiSelector 95.1 kB (+190 B) · 16.8 kB CSS (0) — — · — · —
TopNavMenu 74.9 kB (+188 B) · 11.2 kB CSS (0) — — · — · —
TabMenu 72.1 kB (+181 B) · 11.1 kB CSS (0) — — · — · —
DateInput 187.2 kB (+175 B) · 17.0 kB CSS (0) — — · — · —
DateTimeInput 208.9 kB (+175 B) · 18.7 kB CSS (0) — — · — · —
SideNavItem 70.8 kB (+175 B) · 11.1 kB CSS (0) — — · — · —
TopNavHeading 75.5 kB (+173 B) · 11.2 kB CSS (0) — — · — · —
ChatComposerInput 76.4 kB (+169 B) · 11.3 kB CSS (0) — — · — · —
SideNavHeading 75.9 kB (+169 B) · 11.3 kB CSS (0) — — · — · —
BreadcrumbItem 85.0 kB (+168 B) · 12.1 kB CSS (0) — — · — · —
ChatComposerTokenElement 76.4 kB (+167 B) · 11.3 kB CSS (0) — — · — · —
Pagination 110.1 kB (+165 B) · 17.8 kB CSS (0) — — · — · —
ChatComposer 78.7 kB (+162 B) · 12.0 kB CSS (0) — — · — · —
DateRangeInput 93.0 kB (+161 B) · 12.5 kB CSS (0) — — · — · —
MoreMenu 97.7 kB (+157 B) · 17.6 kB CSS (0) — — · — · —
TopNavMegaMenu 70.0 kB (+157 B) · 11.8 kB CSS (0) — — · — · —
Popover · Storybook 91.2 kB (+145 B) · 14.2 kB CSS (0) 1,549 (+9) ✓ · ✓ · ✓
ComplexSelector 71.3 kB (+139 B) · 11.8 kB CSS (0) — — · — · —
DropdownMenu 97.4 kB (+135 B) · 17.6 kB CSS (0) — — · — · —
Drawer 50.8 kB (+116 B) · 8.3 kB CSS (0) — — · — · —
ContextMenu 108.4 kB (+3 B) · 17.4 kB CSS (0) — — · — · —
TableHeaderCell 109.2 kB (+2 B) · 17.7 kB CSS (0) — — · — · —
TableCell 109.7 kB (+1 B) · 17.9 kB CSS (0) — — · — · —
AlertDialog 105.6 kB (0) · 14.6 kB CSS (0) — — · — · —
AppShell 91.1 kB (0) · 13.5 kB CSS (0) — — · — · —
BottomSheet 68.3 kB (0) · 7.7 kB CSS (0) — — · — · —
CommandPalette 73.3 kB (0) · 12.7 kB CSS (0) — — · — · —
Dialog 24.0 kB (0) · 6.9 kB CSS (0) — — · — · —
DropdownMenuSubMenu 70.4 kB (0) · 9.7 kB CSS (0) — — · — · —
Lightbox 122.6 kB (0) · 10.6 kB CSS (0) — — · — · —
MobileNav 70.7 kB (0) · 10.6 kB CSS (0) — — · — · —
TimeInput 162.5 kB (0) · 16.0 kB CSS (0) — — · — · —
TopNav 72.4 kB (0) · 10.9 kB CSS (0) — — · — · —
TypeaheadItem 6.4 kB (0) · 3.6 kB CSS (0) — ✓ · ✓ · ✓
BottomSheetSwitcher 17.9 kB (−1 B) · 3.4 kB CSS (0) — — · — · —
Markdown 187.7 kB (−1 B) · 20.6 kB CSS (0) — — · — · —
Table 125.0 kB (−1 B) · 18.3 kB CSS (0) — — · — · —
SideNav 114.2 kB (−5 B) · 11.4 kB CSS (0) — — · — · —
lab/ChatEmojiPicker 106.3 kB (new) · 773 B CSS (new) — — · — · —
lab/ChatReactionBar 107.4 kB (new) · 1.1 kB CSS (new) — — · — · —
lab/CommentComposer 152.3 kB (new) · 1.9 kB CSS (new) — — · — · —
lab/Comments 351.5 kB (new) · 2.5 kB CSS (new) — — · — · —
lab/CommentThread 344.6 kB (new) · 2.4 kB CSS (new) — — · — · —
lab/TourStep 97.6 kB (new) · 590 B CSS (new) — — · — · —
lab/TransferListSelector 109.3 kB (new) · 1.4 kB CSS (new) — — · — · —
richtext/RichTextEditorToolbar 278.4 kB (new) · 454 B CSS (new) — — · — · —

Package size: measured; main has no measurement of the merge base to compare yet.

API changes: cli +22 · ~7 · −0 (against the merge base's committed report); core +16 · ~0 · −0; theme-butter +1 · ~0 · −0 (against the merge base's committed report); theme-y2k +1 · ~0 · −0 (against the merge base's committed report)

Committed API report behind this build: cli 29 rows, core 36 rows, theme-butter 1 row, theme-y2k 1 row; run pnpm api:report.

47 API changes (advisory)
Package Where Signature Usage
+ cli ./api · Functions · function exportSearchIndex function exportSearchIndex @experimental: (options?: { cwd?: string | undefined; package?: string | undefined }) => Promise<SearchIndex> import {exportSearchIndex} from '@astryxdesign/cli/api'; exportSearchIndex(…)
+ cli ./api · Functions · function rankSearchIndex function rankSearchIndex @experimental: (query: string, indexes: SearchIndex | SearchIndex[], options?: { type?: SearchDomain | undefined; limit?: number | undefined }) => Promise<SearchResponse> import {rankSearchIndex} from '@astryxdesign/cli/api'; rankSearchIndex(…)
+ cli ./api · Types · type DocsListChild type DocsListChild = DocsNodeChild & { topic: string; description: string; children?: DocsListChild[]; childCount?: number } import type {DocsListChild} from '@astryxdesign/cli/api'
+ cli ./api · Types · type DocsListNamespace childCount?: number | undefined import type {DocsListNamespace} from '@astryxdesign/cli/api'; const x: DocsListNamespace = {childCount: …}
+ cli ./api · Types · type DocsListNamespace children?: DocsListChild[] | undefined import type {DocsListNamespace} from '@astryxdesign/cli/api'; const x: DocsListNamespace = {children: …}
+ cli ./api · Types · type SearchIndex type SearchIndex import type {SearchIndex} from '@astryxdesign/cli/api'
+ cli ./api · Types · type SearchResultEntry deprecated?: ComponentDeprecation | undefined import type {SearchResultEntry} from '@astryxdesign/cli/api'; const x: SearchResultEntry = {deprecated: …}
+ cli ./authoring · Types · interface ComponentDeprecation interface ComponentDeprecation import type {ComponentDeprecation} from '@astryxdesign/cli/authoring'
+ cli ./authoring · Types · interface ComponentIconSlotDoc interface ComponentIconSlotDoc import type {ComponentIconSlotDoc} from '@astryxdesign/cli/authoring'
+ cli ./authoring · Types · interface ComponentIconSlotStateDoc interface ComponentIconSlotStateDoc import type {ComponentIconSlotStateDoc} from '@astryxdesign/cli/authoring'
+ cli ./authoring · Types · interface MultiComponentDoc deprecated?: ComponentDeprecation import type {MultiComponentDoc} from '@astryxdesign/cli/authoring'; const x: MultiComponentDoc = {deprecated: …}
+ cli ./authoring · Types · interface SingleComponentDoc deprecated?: ComponentDeprecation import type {SingleComponentDoc} from '@astryxdesign/cli/authoring'; const x: SingleComponentDoc = {deprecated: …}
+ cli ./doc · Types · interface ComponentBaseDoc deprecated?: ComponentDeprecation import type {ComponentBaseDoc} from '@astryxdesign/cli/doc'; const x: ComponentBaseDoc = {deprecated: …}
+ cli ./doc · Types · interface SubComponentDoc deprecated?: ComponentDeprecation import type {SubComponentDoc} from '@astryxdesign/cli/doc'; const x: SubComponentDoc = {deprecated: …}
+ cli ./doc · Also exported from an earlier entry point · ComponentDeprecation ComponentDeprecation (./authoring) import {ComponentDeprecation} from '@astryxdesign/cli/doc'
+ cli ./doc · Also exported from an earlier entry point · ComponentIconSlotDoc ComponentIconSlotDoc (./authoring) import {ComponentIconSlotDoc} from '@astryxdesign/cli/doc'
+ cli ./doc · Also exported from an earlier entry point · ComponentIconSlotStateDoc ComponentIconSlotStateDoc (./authoring) import {ComponentIconSlotStateDoc} from '@astryxdesign/cli/doc'
+ cli ./json · Also exported from an earlier entry point · DocsListChild DocsListChild (./api) import {DocsListChild} from '@astryxdesign/cli/json'
+ cli ./json · Also exported from an earlier entry point · SearchIndex SearchIndex (./api) import {SearchIndex} from '@astryxdesign/cli/json'
+ cli ./template · Also exported from an earlier entry point · ComponentDeprecation ComponentDeprecation (./authoring) import {ComponentDeprecation} from '@astryxdesign/cli/template'
+ cli ./template · Also exported from an earlier entry point · ComponentIconSlotDoc ComponentIconSlotDoc (./authoring) import {ComponentIconSlotDoc} from '@astryxdesign/cli/template'
+ cli ./template · Also exported from an earlier entry point · ComponentIconSlotStateDoc ComponentIconSlotStateDoc (./authoring) import {ComponentIconSlotStateDoc} from '@astryxdesign/cli/template'
~ cli ./api · Functions · function search function search: (query: string, options?: { cwd?: string | undefined; type?: "template" | "component" | "hook" | "theme" | "doc" | undefined; limit?: number | undefined }) => Promise<SearchResponse> → function search: (query: string, options?: { cwd?: string | undefined; type?: "template" | "component" | "doc" | "theme" | "hook" | undefined; limit?: number | undefined }) => Promise<SearchResponse> import {search} from '@astryxdesign/cli/api'; search(…)
~ cli ./api · Types · type SwizzleCopyResponse data: { component: string; package: string; outputDir: string; filesCopied: number; files: string[]; usesStyleX: boolean; feedback?: SwizzleFeedback | undefined } → data: { component: string; package: string; outputDir: string; filesCopied: number; files: string[]; usesStyleX: boolean; dependencies: Record<string, string>; marker … #84d9505b import type {SwizzleCopyResponse} from '@astryxdesign/cli/api'; const x: SwizzleCopyResponse = {data: …}
~ cli ./authoring · Types · interface MultiComponentDoc theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } import type {MultiComponentDoc} from '@astryxdesign/cli/authoring'; const x: MultiComponentDoc = {theming: …}
~ cli ./authoring · Types · interface SingleComponentDoc theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } import type {SingleComponentDoc} from '@astryxdesign/cli/authoring'; const x: SingleComponentDoc = {theming: …}
~ cli ./authoring · Types · type TemplateCategory type TemplateCategory = | 'Dashboard - Analytics' | 'Dashboard - Comparison' | 'Dashboard - KPI Summary' | 'Dashboard - Monitoring' | 'Dashboard - Executive Summary' | 'Dashboard - Sco … #e7c18378 → type TemplateCategory = | 'Dashboard - Analytics' | 'Dashboard - Comparison' | 'Dashboard - KPI Summary' | 'Dashboard - Monitoring' | 'Dashboard - Executive Summary' | 'Dashboard - Sco … #a8be998f import type {TemplateCategory} from '@astryxdesign/cli/authoring'
~ cli ./doc · Types · interface ComponentBaseDoc theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } import type {ComponentBaseDoc} from '@astryxdesign/cli/doc'; const x: ComponentBaseDoc = {theming: …}
~ cli ./doc · Types · interface SubComponentDoc theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } import type {SubComponentDoc} from '@astryxdesign/cli/doc'; const x: SubComponentDoc = {theming: …}
+ core . · Components · component BaseTypeahead prop handleRef?: React.Ref<BaseTypeaheadHandle> <BaseTypeahead handleRef={…} />
+ core . · Components · component BaseTypeahead prop menu?: BaseTypeaheadMenu <BaseTypeahead menu={…} />
+ core . · Components · component Tokenizer prop menu?: BaseTypeaheadMenu <Tokenizer menu={…} />
+ core . · Components · component Tokenizer prop onOpenChange?: (isOpen: boolean) => void <Tokenizer onOpenChange />
+ core . · Components · component Typeahead prop handleRef?: React.Ref<BaseTypeaheadHandle> <Typeahead handleRef={…} />
+ core . · Components · component Typeahead prop menu?: BaseTypeaheadMenu <Typeahead menu={…} />
+ core . · Types · interface BaseTypeaheadDialogMenuInfo interface BaseTypeaheadDialogMenuInfo import type {BaseTypeaheadDialogMenuInfo} from '@astryxdesign/core'
+ core . · Types · interface BaseTypeaheadHandle interface BaseTypeaheadHandle import type {BaseTypeaheadHandle} from '@astryxdesign/core'
+ core . · Types · interface BaseTypeaheadListboxMenuInfo interface BaseTypeaheadListboxMenuInfo import type {BaseTypeaheadListboxMenuInfo} from '@astryxdesign/core'
+ core . · Types · type BaseTypeaheadMenu type BaseTypeaheadMenu = { role: 'listbox'; render?: (info: BaseTypeaheadListboxMenuInfo) => ReactNode } | { role: 'dialog'; label: string; render: (info: BaseTypeaheadDialogMenuInfo) => ReactNode } import type {BaseTypeaheadMenu} from '@astryxdesign/core'
+ core . · Types · interface TokenizerHandle close: () => void import type {TokenizerHandle} from '@astryxdesign/core'; const x: TokenizerHandle = {close: …}
+ core . · Types · interface TokenizerHandle open: () => void import type {TokenizerHandle} from '@astryxdesign/core'; const x: TokenizerHandle = {open: …}
+ core ./Typeahead · Also exported from an earlier entry point · BaseTypeaheadDialogMenuInfo BaseTypeaheadDialogMenuInfo (.) import {BaseTypeaheadDialogMenuInfo} from '@astryxdesign/core/Typeahead'
+ core ./Typeahead · Also exported from an earlier entry point · BaseTypeaheadHandle BaseTypeaheadHandle (.) import {BaseTypeaheadHandle} from '@astryxdesign/core/Typeahead'
+ core ./Typeahead · Also exported from an earlier entry point · BaseTypeaheadListboxMenuInfo BaseTypeaheadListboxMenuInfo (.) import {BaseTypeaheadListboxMenuInfo} from '@astryxdesign/core/Typeahead'
+ core ./Typeahead · Also exported from an earlier entry point · BaseTypeaheadMenu BaseTypeaheadMenu (.) import {BaseTypeaheadMenu} from '@astryxdesign/core/Typeahead'
+ theme-butter Public attributes · attr data-astryx-pressable attr data-astryx-pressable (public data attribute) [data-astryx-pressable] { … }
+ theme-y2k Public attributes · attr data-astryx-pressable attr data-astryx-pressable (public data attribute) [data-astryx-pressable] { … }

Touched components checked; add ci:run-visual or ci:run-rtl to run the full suites.

Accessibility: 1 violation(s)

Accessibility Audit

Status: 1 accessibility violation(s) found — 1 serious.

Tokenizer - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 2/31 stories · Learn more
    • WCAG: 1.4.3 (Level AA)

Generated by PR Enrichment workflow | View full report

…ctive option

The list loads its entries after the click, and Enter and the arrow keys
act on the active option, so the sibling pick tests raced the same way the
Escape test did. A shared openList() waits for the 'Status' option to be
marked active before any key. Test-only: no product source changes.
@cixzhang

cixzhang commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec ff4d67a

@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.

This pull request is reviewed as part of its stack; see the review on #10625.

…angeset sentence

CI's strict promise-function-async rule rejected the non-async callbacks
passed to expect.poll. Test and changeset text only.

@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.

This pull request is reviewed as part of its stack; see the review on #10625.

The stack's top conflicted with main in the CLI goldens; main is merged at
the bottom and carried up, so each PR's increment over the one below stays
its own change.
@cixzhang

cixzhang commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 8aa37d5

@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.

Changes requested: one scope fix, then nothing else blocks.

  • Regression: this head no longer contains #10544's head, because #10544 merged main after this push, so the declared stack #10544 → #10624 → #10625 does not hold and this pull request's 2,829 authored lines against main are over the 2,000-line limit; merge #10544's current head into this branch, then this branch into #10625, so the stack holds again and the size becomes a non-blocking note.

An engineering owner approved the record changes for this head, and the forwarded menu, the open handle and the token focus return behave as component:Tokenizer FR28 and component:BaseTypeahead FR13–FR16 describe, in the tests and in Chromium. Optional, if you touch the branch: the comment at Tokenizer.tsx:1579-1582 and the 16-line header of BaseTypeaheadFocusContext.ts could be shorter, and FR28's sheet sentence has no test.

[Automated review]

@cixzhang

cixzhang commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 9f908c0

@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.

Changes requested: the stack has to hold again before this pull request can be judged on its Tokenizer increment.

  • The pull request is 2,829 authored lines against main because #10625 does not yet contain this head, so the declared stack #10544 → #10624 → #10625 does not hold and its size exemption does not apply; merge feat/tokenizer-menu into #10625's branch.
  • Newly live check: BaseTypeaheadProps.handleRef and BaseTypeaheadProps.menu are outside this pull request's scope (Tokenizer): name them in the scope and get an owner decision, or split them into their own pull request.
  • Newly live check: BaseTypeaheadMenu is outside this pull request's scope (Tokenizer): name it in the scope and get an owner decision, or split it into its own pull request.
  • Newly live check: BaseTypeaheadHandle is outside this pull request's scope (Tokenizer): name it in the scope and get an owner decision, or split it into its own pull request.
  • Newly live check: BaseTypeaheadListboxMenuInfo is outside this pull request's scope (Tokenizer): name it in the scope and get an owner decision, or split it into its own pull request.
  • Newly live check: BaseTypeaheadDialogMenuInfo is outside this pull request's scope (Tokenizer): name it in the scope and get an owner decision, or split it into its own pull request.
  • Newly live check: TypeaheadProps.handleRef and TypeaheadProps.menu are outside this pull request's scope (Tokenizer): name them in the scope and get an owner decision, or split them into their own pull request.

These rows are #10544's, so the same merge fixes them: once the stack holds, this pull request's own increment changes only Tokenizer rows. The record changes are owner-approved for this head, and the forwarded menu, the open handle and the token focus return behave as component:Tokenizer FR28 and component:BaseTypeahead FR13–FR16 describe. Optional, if you touch the branch: the comment at Tokenizer.tsx:1579-1582 and the header of BaseTypeaheadFocusContext.ts could be shorter, and FR28's sheet sentence has no test.

[Automated review]

Comment thread packages/core/api-report.md Resolved

@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.

Approved; CI passes on this head.

#10625 now contains this head, so the declared stack #10544 → #10624 → #10625 holds again and this pull request is judged on its own Tokenizer increment; the size and the BaseTypeahead and Typeahead API rows behind the earlier request for changes belong to #10544. An engineering owner approved component:Tokenizer FR28 for this head, and the forwarded menu and onOpenChange, the handle's open()/close() and the focus return to a token behave as it describes, by mouse, by keyboard and with a click that does not focus the button, as in Safari. This pull request lands only together with #10544 and #10625, once #10544 is approved too.

  • Optional, if you touch the branch: an active-voice comment at Tokenizer.tsx:1579-1582, a shorter BaseTypeaheadFocusContext.ts header, and a test for FR28's sheet sentence.

[Automated review]

@cixzhang

cixzhang commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 9f908c0

@cixzhang

cixzhang commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 9f908c0

This branch was successfully deployed

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

Labels

ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant