Skip to content

feat: keep every portaled component inside a shadow root - #31

Merged
antfu merged 1 commit into
mainfrom
tangy-berries-yawn
Oct 1, 2026
Merged

antfu merged 1 commit into
mainfrom
tangy-berries-yawn

Conversation

@antfubot

@antfubot antfubot commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Closes #30

Why

FormSelect (and every other popper, plus toasts) teleported to document.body, so inside a shadow root the content left the tree holding the stylesheet and rendered unstyled. Only the dialog family had been fixed in #29.

What changes

  • All portaled components detect the enclosing shadow root and teleport into it, with a to?: string | HTMLElement override — FormSelect, FormCombobox, LayoutMenubar, OverlayDropdown, OverlayDropdownSub, OverlayHoverCard, OverlayContextMenu, FeedbackToasts, same as OverlayModal/OverlayDrawer/OverlayConfirm.
  • usePortalTarget resolves the component's own root node instead of taking an anchor ref, so the display: contents wrapper divs the dialogs carried are gone and components whose root is a real element keep inherited attrs where they belong.
  • Dark mode inside the portal container. dark: utilities compile to .dark .x; a container appended to the ShadowRoot had no .dark ancestor, so portaled content rendered light in a dark host. The container now mirrors the dark/light class of the nearest scheme wrapper and follows toggles.
  • shimmer emits :where(.dark) instead of :where(html.dark), so it matches the same descendant convention as every other dark token.
  • test/portal.test.ts opens every portaled component in a shadow root (and in the body) and covers the scheme mirroring; a new Utilities/ShadowRoot story mounts them all inside a shadow root for visual checks. README and AGENTS.md document the convention.

Verified

Light and dark, inside the story's shadow root — zero popups land in document.body:

Dropdown + submenu (dark) Select (dark) Modal + toast (dark)
menus in shadow: 2, in body: 0 listbox in shadow, container class dark both in shadow, none in body

Not in scope: vue-afloat tooltips are already shadow-aware at the library level; the Utilities/* Overview links were already off (utility- vs utilities-) before this change.


Created with the help of an agent.

Selects, comboboxes, menus, hover cards, context menus and toasts now resolve
their teleport target through usePortalTarget like the dialogs already did,
and expose a `to` override. The shadow-root container mirrors the nearest
dark/light scheme class so dark tokens still apply to portaled content, and
`shimmer` no longer hard-codes html.dark.

Closes #30
@antfu
antfu merged commit ceb5205 into main Oct 1, 2026
5 checks passed
@antfu
antfu deleted the tangy-berries-yawn branch October 1, 2026 00:42
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.

FormSelect: dropdown teleports to document.body and loses styles inside a shadow root

2 participants