Skip to content

fix(hub-ui,json-render-ui): FloatingPopover escapes a transformed ancestor's containing block; native Select mode - #228

Open
antfubot wants to merge 1 commit into
mainfrom
fix/floating-popover-transformed-ancestor-native-select
Open

fix(hub-ui,json-render-ui): FloatingPopover escapes a transformed ancestor's containing block; native Select mode#228
antfubot wants to merge 1 commit into
mainfrom
fix/floating-popover-transformed-ancestor-native-select

Conversation

@antfubot

Copy link
Copy Markdown
Collaborator

What changed

  • FloatingPopover (hub-ui) resolves the outermost ancestor that forms a containing block for position: fixed descendants and <Teleport>s its panel into that ancestor's parent. With no such ancestor, it renders in place as before.
  • Select (json-render-ui) gains native, rendering a real <select> instead of FormSelect/FormCombobox. It has no icon, description or searchable support.
  • SelectPropsSchema (json-render) gains the matching native field, so a spec can opt an element into it and the tsnapi API snapshot is updated.
  • New Storybook story (EscapesTransformedAncestor) demonstrating the escape fix against a transformed, overflow-clipping ancestor.

Why it changed

A position: fixed panel is positioned relative to its nearest ancestor carrying a non-none transform/filter/contain, not the viewport, and is clipped by that ancestor's overflow. Any of FloatingPopover's current consumers — the dock overflow panel, group popover, context menu, edge-position dropdown — would be mispositioned and clipped if rendered inside such an ancestor. Escaping the outermost rather than the nearest such ancestor matters because escaping one can land inside another. Walking parentElement (not parentNode) naturally stops at a shadow root's boundary, so the teleport target stays inside the dock's shadow root — where its stylesheet is scoped — rather than escaping to document.body and losing its styling.

native on Select is an escape hatch worth having regardless: the browser draws a native option list outside the page's layout, so no ancestor can clip or misposition it at all — the dependable choice for a Select embedded in a host layout this component doesn't control.

Porting notes

This ports vitejs/devtools#518 ("fix(core): FloatingPopover escapes a transformed ancestor's containing block; native Select mode" by @dvcolomban), per #205. That PR targeted packages/core/src/client/webcomponents/** in vitejs/devtools, which moved here as packages/hub-ui/src/client/components/floating/** and packages/json-render-ui/src/components/Select.ts in the v0.9 migration.

Adapted rather than ported verbatim:

  • The original PR's surface prop on FloatingPopover (tooltip | menu) and Select's dropdown switching to surface="menu" don't have an equivalent here — this fork's Select renders through @antfu/design's FormSelect/FormCombobox (reka-ui popovers), not through FloatingPopover, so there's no consumer to point a menu surface at. FloatingPopover's own five menu-style consumers (dock overflow, group popover, context menu, edge-position dropdown) are left as-is — restyling their padding is a separate, visually-verifiable change I didn't want to bundle into a positioning bug fix.
  • native's styling is written against this fork's design-system semantic classes (border-base, bg-base, color-base, …) per this repo's conventions, rather than the upstream fix's inline styles.
  • Worth flagging: @antfu/design's FormSelect/FormCombobox (reka-ui SelectPortal/ComboboxPortal) default to portalling into document.body, which suffers the same class of positioning/clipping bug inside a shadow root or transformed ancestor. Fixing that is out of scope here (it's an upstream dependency, not first-party code) but may be worth its own follow-up issue.

Credit to @dvcolomban for the original fix and rationale.

Closes #205


Prepared with the help of an agent.

…estor's containing block; native Select mode

`FloatingPopover` (hub-ui) is positioned `fixed` against its anchor's viewport rect, but a
`transform`/`filter`/`contain` ancestor makes itself the containing block for that
`position: fixed`, so the panel ends up positioned relative to — and clipped by — that
ancestor instead of the viewport. `resolveFixedEscapeTarget` walks up from the anchor to the
outermost such ancestor (escaping only the nearest one can land inside another) and
`<Teleport>`s the panel to its parent; walking `parentElement` stops at a shadow root's
boundary, so a dock's popover never escapes the shadow root its stylesheet is scoped to. With
no such ancestor, the panel renders in place as before.

`Select` (json-render-ui) gains `native`, rendering a real `<select>` instead of
`FormSelect`/`FormCombobox`: the browser draws its option list outside the page's layout, so
no ancestor can clip or reposition it at all — a dependable escape hatch for a `Select`
embedded in a host layout this component doesn't control, at the cost of `icon`,
`description` and `searchable`, which have no native equivalent.

Ports vitejs/devtools#518, adapted to this fork: that PR's `surface` prop and `Select`'s
switch to it don't have an equivalent here — this fork's `Select` renders through
@antfu/design's `FormSelect`/`FormCombobox` (reka-ui popovers), not through
`FloatingPopover`, so there's no consumer to point at a menu surface. (Those reka-ui
popovers default to portalling into `document.body`, which suffers the same class of bug
inside a shadow root or a transformed ancestor — worth a follow-up, but out of scope for a
first-party fix here.)

Closes #205

Co-authored-by: dvcolomban <dinh-van.colomban@contentsquare.com>
@netlify

netlify Bot commented Aug 14, 2026

Copy link
Copy Markdown

Deploy Preview for devfra ready!

Name Link
🔨 Latest commit 43b8386
🔍 Latest deploy log https://app.netlify.com/projects/devfra/deploys/6a7f00d7d7566c0008e8f427
😎 Deploy Preview https://deploy-preview-228--devfra.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

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.

Port fix from vitejs/devtools#518: FloatingPopover escapes a transformed ancestor's containing block; native Select mode

1 participant