Skip to content

feat(selector): open the menu below by default - #4976

Open
AKnassa wants to merge 1 commit into
facebook:mainfrom
AKnassa:rocky/issue-4227-selector-below-default
Open

feat(selector): open the menu below by default#4976
AKnassa wants to merge 1 commit into
facebook:mainfrom
AKnassa:rocky/issue-4227-selector-below-default

Conversation

@AKnassa

@AKnassa AKnassa commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Closes #4227

What this does

Selector's menu now opens underneath the button, with the same small gap every other Astryx menu uses. The old behavior — the menu covering the button with the selected option pinned on top — is still available per instance via a new hasSelectedItemOverlay prop.

Why

Following up my analysis on the issue: the overlay default covered the very button that keeps keyboard focus (SC 2.4.11 territory), quietly fell apart once the selected option sat past the listbox fold (the menu just pinned to the top of the screen — including the empty placeholder state every form starts in), and made default Selector the odd one out: DropdownMenu, MultiSelector, ComplexSelector, and Selector-with-search all open below already.

The three calls left open on the issue — proposed here, easy to veto piecemeal

  1. Does the overlay survive? Yes, as opt-in — the issue's acceptance asks that consumers can get back to it, and opt-in is the reversible choice.
  2. What's it called? hasSelectedItemOverlay — the has* convention, anchored to "selected item" (the code's own term), since bare "overlay" already means the Overlay component and its tokens.
  3. The gap? --spacing-1, the standard clearance DropdownMenu, MultiSelector, and ComplexSelector already use. Search-mode Selector gains it too (it used to sit flush).

What changed

  • Menus open below with the standard gap by default; hasSelectedItemOverlay restores the overlay per instance (only on below placement — explicit above/start/end still wins, like today).
  • placement is now documented — it was missing from the docsite props table and astryx component Selector --dense entirely (the "(b)" half of the issue, worth having either way).
  • New Storybook story Selected Item Overlay beside the existing placement stories.
  • Changeset [breaking] → minor: every default Selector changes visually.

How to see it

Storybook → Core/Selector: Default now opens below with the gap; Selected Item Overlay shows the opt-in.

Test plan

  • 3 new tests written red-first against untouched main, green after: default = below + --spacing-1 on both block edges (flip-safe per [Bug] ComplexSelector popup has no offset when placement="above" #4803), explicit placements get the clearance, search mode gets it and never overlays.
  • The old overlay geometry tests (viewport clamp, untransformed-layout alignment) now run under hasSelectedItemOverlay and still pass — the measurement path is untouched.
  • Full suite 9887 passing (the two macOS case-sensitivity failures in packages/cli are pre-existing and unrelated); docsite 372 passing; pnpm lint:strict, pnpm test, pnpm build all green.

The selected-item overlay — the open menu pulled up so the selected
option sits over the trigger, native-select style — is no longer the
default. Selector now opens below with the standard --spacing-1
clearance, the same recipe DropdownMenu, MultiSelector, and
ComplexSelector already use; search mode gains the clearance too (it
used to sit flush). The overlay survives as opt-in
hasSelectedItemOverlay, and only engages on below placement — an
explicit above/start/end still wins, as before.

Why the flip, measured rather than assumed:
- The trigger keeps DOM focus while the popup is open and the overlay
  covers it entirely — SC 2.4.11 (AA) focus-obscured territory.
- The overlay quietly degenerates: with the selected option past the
  listbox fold the menu pins to the viewport top (the measured margin
  equals triggerRect.bottom), and the no-value placeholder state that
  every form starts in still pulls the menu over its own trigger.
- It was already only half the default: hasSearch switches to plain
  below, MultiSelector hard-codes below, and every other menu-shaped
  popup in core opens below.

placement was also undocumented everywhere that matters — absent from
Selector.doc.mjs, so missing from the docsite props table and from
astryx component Selector --dense. Both it (@default 'below') and the
new prop are documented now.

Mechanically small: overlay mode was already below placement plus a
measured negative margin-block-start, so the flip removes the margin
and adds the offset; useSelectedItemOffset is untouched and stays
public API. The clearance is gated off in overlay mode — offset sets
both block edges (flip-safe per facebook#4803) while the overlay margin is
inline, and mixing them would offset the far edge asymmetrically.

Verified: 3 new tests red against the untouched tree, then green; 113
pass in Selector.test.tsx; full suite 9887 pass with only the two
pre-existing macOS cli-casing failures from the recorded baseline;
docsite 372 pass; core tsc, docsite and storybook typecheck, CI=true
lint, check:repo (changeset included), and prettier all clean.
Storybook screenshots confirm both modes.

Changeset: [breaking] minor per the 0.x convention. To restore the old
look on an instance, add hasSelectedItemOverlay.

Refs facebook#4227
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview Aug 13, 2026 3:53am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 13, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge labels Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

Modified Components

Selector (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 1479 -
Complexity N/A Very High (155) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.7KB 1.2KB

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@cixzhang

Copy link
Copy Markdown
Contributor

On the naming call (#2 in your list): rather than a new boolean, I'd rather we expand placement itself — add 'overlay' and 'offset' alongside the four directions, so the prop that already decides where the menu sits carries this too:

placement?: 'above' | 'below' | 'start' | 'end' | 'overlay' | 'offset';
  • 'overlay' — the selected option over the trigger, today's default behavior, now nameable instead of implied by omitting the prop.
  • 'offset' — clear of the trigger by the standard menu gap, direction left to the layer. The semantic default option (c) that #4227 asked about, without hard-coding a direction the way 'below' does.

Two reasons I prefer this to hasSelectedItemOverlay. It keeps one prop answering one question — with a boolean, placement="above" hasSelectedItemOverlay is a combination we have to define and then explain. And it gives the overlay a name, which we need anyway: #5004 shows the overlay is only sound while the selected option actually covers the trigger (with no selection, or when the viewport clamp slides another option there, a press meant to dismiss the menu commits whatever is painted on top), so the component has to be able to fall back to offset placement on its own — and "fall back to 'offset'" is a sentence we can write in the docs.

Separately, I'm not sure yet that we want to change the default at all — that's a bigger call than the naming, and I'd like to keep it separate from the API shape. Would you split this into the placement API (values + docs, no behavior change) and the default flip? The first can land immediately.

Also worth knowing: #5018 touches Selector.tsx near the trigger, so a small rebase either way.

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

Thanks for this — your analysis on the issue did most of the work, and the placeholder case is the sharpest version of it: before anyone has picked anything, the trigger is completely hidden behind its own menu.

Following up on my comment above, plus one thing that changed since you opened this: the --spacing-1 clearance landed on main as #5003, so it comes out of your diff and takes most of the conflict with it.

That leaves the placement API half quite small — could you send just that first?

  • 'overlay' and 'offset' on LayerPlacement, behavior unchanged · useLayer.tsx:72
  • placement in the props table, still missing on main · Selector.doc.mjs:85

The flip, the changeset and the overlay story then ride on a second PR, where the default is the only thing left to decide.

If you'd rather talk it through with someone, we're in Discord.

[Reviewed by Robohands]

* hasSearch uses standard layer positioning instead.
* @default false
*/
hasSelectedItemOverlay?: boolean;

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.

Superseded by the 'overlay' / 'offset' placement values. Comes out with the flip.

'@astryxdesign/core': minor
---

[breaking] Selector menus open below the trigger by default (#4227). The old native-select behavior — the open menu overlaying the trigger with the selected option pinned over it — is now opt-in via `hasSelectedItemOverlay`. `placement` keeps working as before and now documents its `'below'` default; Selector menus also gain the standard `--spacing-1` clearance that DropdownMenu, MultiSelector, and ComplexSelector already use (search mode included, which used to sit flush).

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.

Search mode isn't flush any more, #5003 landed that. Needs a reword.

@cixzhang

Copy link
Copy Markdown
Contributor

Checking in on Selector placement: the current review asks to split the small placement API/docs change from the default-behavior decision. Would you like to continue, have us carry it, or close this for now?

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. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Selector: open the menu underneath the trigger (reconsider overlay-over-trigger default)

2 participants