feat(ContextMenu): prototype searchable async option loading - #3085
Draft
mfal wants to merge 2 commits into
Draft
Conversation
Engineering groundwork for searchable, async-loaded option lists in
ContextMenu. A prototype, not a public component: the interaction and
visual design belong to UX, so nothing is exported from public.ts or
flr-universal.ts and no component is @flr-generate annotated.
Lives in src/prototypes/, outside the src/{components,integrations} glob
that the docgen and remote-component generators scan, so it cannot leak
into a generated surface.
What it establishes:
- Everything needed ships in react-aria-components 1.20.0 already —
Autocomplete, useFilter, useAsyncList, Virtualizer, ListLayout, all
stable. No new dependency. The repo used only useFilter so far.
- Selection is independent of what is loaded. selectedKeys lives above
the loader, which never sees it; an option cache keeps a selected
option renderable after its page is gone. mergeSelection pins the
invariant, and the browser test fails if react-aria ever stops
preserving out-of-collection keys (measured: 1.20 preserves them).
- A superseded load never lands: every request runs under an
AbortController, so typing "pro" then "prod" cannot leave the "pro"
page on screen.
- The ceiling is rendering, not fetching or filtering. Toggling one
option costs ~70 ms at 25 rendered items and ~350 ms at 2000, because
react-aria rebuilds the collection on every selection change. Paging
is what avoids it; the UnpagedStress story is there to measure it.
Deliberately open: the two treatments for a selected-but-filtered-out
option (pin vs. inline) are both implemented so UX can compare them, and
the search field and load-more control are unstyled.
part of #1851
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mfal
force-pushed
the
feat/context-menu-async-prototype-1851
branch
from
September 2, 2026 11:53
0f9898a to
a788f05
Compare
Contributor
🚀 Preview DeploymentPreview environments are ready:
Images:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prototype, not a feature
Searchable, async-loaded, multi-select option menu for
ContextMenu— built to answer the three challenges in #1851 and to give UX something concrete to react to. It is not meant to be merged as a public component.part of #1851— notfixes, because UX has to decide the open questions before this can ship.Full write-up, including the API proposal and the remote-boundary analysis: #1851 (comment)
Why a prototype
A searchable async menu is a new interaction surface. Its visual and interaction design is UX's call, not the implementer's, and base design tokens are off limits. So this establishes the mechanics and the constraints, and stops there.
What it proves
react-aria-components@1.20.0—Autocomplete,useFilter,useAsyncList,Virtualizer,ListLayout, all stable, noUNSTABLE_. No new dependency. The repo used onlyuseFilterbefore this.selectedKeyslives above the loader, which never sees it. An option cache keeps a selected option renderable after its page is gone — keeping the keys alone is not enough, because react-aria only renders items in the collection.mergeSelectionbypassed). The helper stays to pin that invariant against a minor upgrade and to resolve the"all"wildcard, which is collection-bound.AbortController, so typing "pro" then "prod" cannot leave the "pro" page on screen.UnpagedStressstory exists to measure it.on*props discard their return value, a non-on*function property does round-trip through@quilted/threads(asXAxis.tickFormatteralready does), and the design that needs neither is the right one.What is deliberately missing
public.tsorflr-universal.ts; no component is@flr-generate. No generated artifact changes — verified withgit statusafter a fulltest:compilerun, which runs the generators.*.module.d.scss.ts).pinandinline— so UX can compare rather than be handed one.locales/*), no docs page, noremote-dom-demopage, no visual tests. All belong to the real component.Selector theListfilter menus. The real implementation belongs inOptions(shared bySelect,ComboBox,Autocomplete) so they all get it at once.Placement
packages/components/src/prototypes/contextMenuAsync/— deliberately outsidesrc/{components,integrations}, the glob thatcreateDocPropertiesJsonand the remote-component generator scan. It therefore cannot leak intodoc-properties.json, the component index, or any remote artifact. Storybook still picks the stories up (src/**/*.stories.tsx).Verification
pnpm nx test:unit components— 260 passed, including 7 new for the selection mergepnpm nx test:compile components— cleanpnpm lint— 0 errors, 0 warnings in the new filespnpm nx test:browser components --browser.name=webkiton the new test — 8 passed (4 tests × 2 projects), covering: selection surviving a filter that hides it, a pinned selection staying visible and uncheckable, selection surviving a second page load, and the trigger count staying right across filter changesStories
Prototypes/AsyncOptionMenu—Default,PinSelected,InlineSelected,SlowBackend,SinglePage,UnpagedStress. Backed by a fake backend that filters and pages server-side over 5000 options, so the client never holds the universe.