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

@Zayooo00Zayooo00 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
Zayooo00force-pushed the feat/combobox-headlessui-v2-anchor branch from f9d6bed to ff18b03CompareAugust 31, 2026 10:49
@Zayooo00
Zayooo00 enabled auto-merge (squash) August 31, 2026 10:56
@Zayooo00Zayooo00 self-assigned this Aug 31, 2026
@Zayooo00
Zayooo00 merged commit 326dc32 into mainAug 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 freeto 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

@Zayooo00@dosmiko7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@Zayooo00Zayooo00 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
Zayooo00force-pushed the feat/combobox-headlessui-v2-anchor branch from f9d6bed to ff18b03CompareAugust 31, 2026 10:49
@Zayooo00
Zayooo00 enabled auto-merge (squash) August 31, 2026 10:56
@Zayooo00Zayooo00 self-assigned this Aug 31, 2026
@Zayooo00
Zayooo00 merged commit 326dc32 into mainAug 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 freeto 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

@Zayooo00@dosmiko7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@Zayooo00Zayooo00 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
Zayooo00force-pushed the feat/combobox-headlessui-v2-anchor branch from f9d6bed to ff18b03CompareAugust 31, 2026 10:49
@Zayooo00
Zayooo00 enabled auto-merge (squash) August 31, 2026 10:56
@Zayooo00Zayooo00 self-assigned this Aug 31, 2026
@Zayooo00
Zayooo00 merged commit 326dc32 into mainAug 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 freeto 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

@Zayooo00@dosmiko7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@Zayooo00Zayooo00 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
Zayooo00force-pushed the feat/combobox-headlessui-v2-anchor branch from f9d6bed to ff18b03CompareAugust 31, 2026 10:49
@Zayooo00
Zayooo00 enabled auto-merge (squash) August 31, 2026 10:56
@Zayooo00Zayooo00 self-assigned this Aug 31, 2026
@Zayooo00
Zayooo00 merged commit 326dc32 into mainAug 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 freeto 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

@Zayooo00@dosmiko7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@Zayooo00Zayooo00 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
Zayooo00force-pushed the feat/combobox-headlessui-v2-anchor branch from f9d6bed to ff18b03CompareAugust 31, 2026 10:49
@Zayooo00
Zayooo00 enabled auto-merge (squash) August 31, 2026 10:56
@Zayooo00Zayooo00 self-assigned this Aug 31, 2026
@Zayooo00
Zayooo00 merged commit 326dc32 into mainAug 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 freeto 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

@Zayooo00@dosmiko7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@Zayooo00Zayooo00 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
Zayooo00force-pushed the feat/combobox-headlessui-v2-anchor branch from f9d6bed to ff18b03CompareAugust 31, 2026 10:49
@Zayooo00
Zayooo00 enabled auto-merge (squash) August 31, 2026 10:56
@Zayooo00Zayooo00 self-assigned this Aug 31, 2026
@Zayooo00
Zayooo00 merged commit 326dc32 into mainAug 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 freeto 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

@Zayooo00@dosmiko7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@Zayooo00Zayooo00 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
Zayooo00force-pushed the feat/combobox-headlessui-v2-anchor branch from f9d6bed to ff18b03CompareAugust 31, 2026 10:49
@Zayooo00
Zayooo00 enabled auto-merge (squash) August 31, 2026 10:56
@Zayooo00Zayooo00 self-assigned this Aug 31, 2026
@Zayooo00
Zayooo00 merged commit 326dc32 into mainAug 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 freeto 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

@Zayooo00@dosmiko7
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@Zayooo00Zayooo00 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
Zayooo00force-pushed the feat/combobox-headlessui-v2-anchor branch from f9d6bed to ff18b03CompareAugust 31, 2026 10:49
@Zayooo00
Zayooo00 enabled auto-merge (squash) August 31, 2026 10:56
@Zayooo00Zayooo00 self-assigned this Aug 31, 2026
@Zayooo00
Zayooo00 merged commit 326dc32 into mainAug 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 freeto 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

@Zayooo00@dosmiko7