Skip to content

feat(Combobox): portal + anchor the dropdown menu (Headless UI v2) (MET-3088) - #62

Merged
Zayooo00 merged 1 commit into
mainfrom
feat/combobox-headlessui-v2-anchor
Aug 31, 2026
Merged

feat(Combobox): portal + anchor the dropdown menu (Headless UI v2) (MET-3088)#62
Zayooo00 merged 1 commit into
mainfrom
feat/combobox-headlessui-v2-anchor

Conversation

@Zayooo00

@Zayooo00 Zayooo00 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Problem

The Combobox dropdown menu renders as a position: absolute child of the input. In a narrow container it is clipped by an overflow ancestor and can render off-screen with no flip/shift — most visibly the department picker in the Institutional Program Assessment sidebar, where long option labels wrap across several lines and a caller-set max-width has no effect.

Change

Migrate the code component from Headless UI v1 → v2 and use anchor on ComboboxOptions, which portals the menu to <body> and positions it with Floating UI (flip + shift + viewport padding).

Behaviour

  • New menuWidth prop.
    • "input" (default) — the menu is exactly as wide as the input. Visually identical to today (anchor gap of 1px + restored top clip-path).
    • "fit" — the menu grows to the widest option (long labels stop wrapping), never narrower than the input. Cap it with max-width on optionsClassName.
  • width / min-width are set inline (they win over a leftover width in optionsClassName); max-width is left to CSS so callers keep control of it.
  • The menu is split into .positioner (portalled + positioned by Floating UI, which stamps an inline max-width: <viewport> onto it every reposition) and an inner .options wrapper that carries all author-facing sizing and styling — so a caller's max-width is no longer clobbered.
  • immediate replaces the old arrowIconRef.click() focus hack; a slim onClick is kept so re-clicking an already-focused input reopens the menu.
  • <Transition> wrapper → transition prop + .positioner[data-closed] (same 0.1s opacity fade). nullable dropped (v2 native). Query resets on onClose.
  • Group headings: <Combobox.Label><div role="presentation"> (same class/text).

Breaking

  • @headlessui/react peer range bumped to ^2.2.0. 2.2.x carries the Combobox open-on-trigger and immediate fixes; installed 2.1.8 had a regression where clicking the trailing button did not open the menu. The only consumer (myevals-plasmic-utilsmyevals-react-frontend) has no other Headless UI usage; it bumps alongside.
  • optionClassName state selectors move from [data-headlessui-state*=active] / [aria-selected=true] / [aria-disabled=true] to v2's [data-focus] / [data-selected] / [data-disabled]. The active / selected / highlighted / disabled option styles need re-verifying in Plasmic Studio after the registration re-syncs.

Verified

  • tsc -p tsconfig.package.json, next lint, vitest (49 pass) green.
  • Local yarn build + link into myevals-react-frontend: input mode matches the current menu; fit mode grows to content and honours a max-width set via optionsClassName; open-on-focus, filtering, empty state, 500-result cap, footer slot, grouped options all work.

Follow-ups (not in this PR)

  • Sync into myevaluations/myevals-plasmic-utils (fork differs only by the org rename).
  • Bump the dep + @headlessui/react in myevals-react-frontend, re-sync Plasmic, re-style option states, add the menuWidth="fit" variant to the department picker.

@notion-workspace

Copy link
Copy Markdown

…-3088)

Migrate the Combobox code component from Headless UI v1 to v2 so the
dropdown menu is portalled and positioned by Floating UI instead of being
an abspos child of the input. This lets the menu escape any `overflow`
ancestor (it was previously clipped inside narrow containers, e.g. the
Institutional Program Assessment sidebar) and flip/shift to stay in the
viewport.

- `Combobox.Input/Button/Options/Option` -> v2 named components; drop the
  `<Transition>` wrapper for the `transition` prop + `[data-closed]`; drop
  `nullable`; reset the query on `onClose`.
- Replace the `arrowIconRef.click()` focus hack with `immediate` (kept a
  slim `onClick` so re-clicking a focused input reopens the menu).
- New `menuWidth` prop: `"input"` (default, unchanged behaviour) or
  `"fit"` (grows to the widest option, never narrower than the input).
  `width`/`min-width` are applied inline so they win over a leftover
  `width` in `optionsClassName`; `max-width` is left to CSS so callers
  can still cap it.
- Split the menu into `.positioner` (what Floating UI portals/positions
  and stamps an inline `max-width: <viewport>` onto) and an inner
  `.options` wrapper that carries all author-facing sizing/look, so a
  caller's `max-width` is no longer overridden.
- `anchor` gap of 1px + restored top `clip-path` keep `"input"` mode
  visually identical to before.
- Option-state selectors for `optionClassName` move from
  `[data-headlessui-state*=active]` etc. to v2's `[data-focus]` /
  `[data-selected]` / `[data-disabled]`.
- Bump the `@headlessui/react` peer range to ^2.2.0 (2.2.x carries the
  Combobox open-on-trigger and `immediate` fixes we need).
@Zayooo00
Zayooo00 force-pushed the feat/combobox-headlessui-v2-anchor branch from f9d6bed to ff18b03 Compare August 31, 2026 10:49
@Zayooo00
Zayooo00 enabled auto-merge (squash) August 31, 2026 10:56
@Zayooo00 Zayooo00 self-assigned this Aug 31, 2026
@Zayooo00
Zayooo00 merged commit 326dc32 into main Aug 31, 2026
2 checks passed
@Zayooo00
Zayooo00 deleted the feat/combobox-headlessui-v2-anchor branch August 31, 2026 10:57
Zayooo00 added a commit to myevaluations/myevals-plasmic-utils that referenced this pull request Aug 31, 2026
…-3088) (fullstackhouse#62) (#4)

Migrate the Combobox code component from Headless UI v1 to v2 so the
dropdown menu is portalled and positioned by Floating UI instead of being
an abspos child of the input. This lets the menu escape any `overflow`
ancestor (it was previously clipped inside narrow containers, e.g. the
Institutional Program Assessment sidebar) and flip/shift to stay in the
viewport.

- `Combobox.Input/Button/Options/Option` -> v2 named components; drop the
  `<Transition>` wrapper for the `transition` prop + `[data-closed]`; drop
  `nullable`; reset the query on `onClose`.
- Replace the `arrowIconRef.click()` focus hack with `immediate` (kept a
  slim `onClick` so re-clicking a focused input reopens the menu).
- New `menuWidth` prop: `"input"` (default, unchanged behaviour) or
  `"fit"` (grows to the widest option, never narrower than the input).
  `width`/`min-width` are applied inline so they win over a leftover
  `width` in `optionsClassName`; `max-width` is left to CSS so callers
  can still cap it.
- Split the menu into `.positioner` (what Floating UI portals/positions
  and stamps an inline `max-width: <viewport>` onto) and an inner
  `.options` wrapper that carries all author-facing sizing/look, so a
  caller's `max-width` is no longer overridden.
- `anchor` gap of 1px + restored top `clip-path` keep `"input"` mode
  visually identical to before.
- Option-state selectors for `optionClassName` move from
  `[data-headlessui-state*=active]` etc. to v2's `[data-focus]` /
  `[data-selected]` / `[data-disabled]`.
- Bump the `@headlessui/react` peer range to ^2.2.0 (2.2.x carries the
  Combobox open-on-trigger and `immediate` fixes we need).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants