Use correct ownerDocument when using internal <Portal/> (#3594)
This PR improves the internal `<Portal>` component by allowing to pass
in a custom `ownerDocument`.
This fixes an issue if you do something like this:
```ts
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { useState } from 'react'
import { createPortal } from 'react-dom'
export default function App() {
let [target, setTarget] = useState(null)
return (
<div className="grid min-h-full place-content-center">
<iframe
ref={(iframe) => {
if (!iframe) return
if (target) return
let el = iframe.contentDocument.createElement('div')
iframe.contentDocument.body.appendChild(el)
setTarget(el)
}}
className="h-[50px] w-[75px] border-black bg-white"
>
{target && createPortal(<MenuExample />, target)}
</iframe>
</div>
)
}
function MenuExample() {
return (
<Menu>
<MenuButton>Open</MenuButton>
<MenuItems
anchor="bottom"
className="flex min-w-[var(--button-width)] flex-col bg-white shadow"
>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
```
---
Here is a little reproduction video. The `<Menu/>` you see is rendered
in an `<iframe>`, the goal is that `<MenuItems/>` _also_ render inside
of the `<iframe>`.
In the video below we start with the fix where you can see that the
items are inside the iframe (and unstyled because I didn't load any
styles). The second part of the video is the before, where you can see
that the `<MenuItems/>` escape the `<iframe>` and are styled. That's not
what we want.
https://github.com/user-attachments/assets/2da7627e-7846-4c4d-bb14-278f80a03cd8
This commit is contained in:
@@ -1685,6 +1685,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
|
|||||||
actions.setOptionsElement,
|
actions.setOptionsElement,
|
||||||
setLocalOptionsElement
|
setLocalOptionsElement
|
||||||
)
|
)
|
||||||
|
let portalOwnerDocument = useOwnerDocument(data.buttonElement || data.inputElement)
|
||||||
let ownerDocument = useOwnerDocument(data.optionsElement)
|
let ownerDocument = useOwnerDocument(data.optionsElement)
|
||||||
|
|
||||||
let usesOpenClosedState = useOpenClosed()
|
let usesOpenClosedState = useOpenClosed()
|
||||||
@@ -1819,7 +1820,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
|
|||||||
let render = useRender()
|
let render = useRender()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal enabled={portal ? props.static || visible : false}>
|
<Portal enabled={portal ? props.static || visible : false} ownerDocument={portalOwnerDocument}>
|
||||||
<ComboboxDataContext.Provider
|
<ComboboxDataContext.Provider
|
||||||
value={data.mode === ValueMode.Multi ? data : { ...data, isSelected }}
|
value={data.mode === ValueMode.Multi ? data : { ...data, isSelected }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -951,6 +951,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
|
|||||||
let data = useData('Listbox.Options')
|
let data = useData('Listbox.Options')
|
||||||
let actions = useActions('Listbox.Options')
|
let actions = useActions('Listbox.Options')
|
||||||
|
|
||||||
|
let portalOwnerDocument = useOwnerDocument(data.buttonElement)
|
||||||
let ownerDocument = useOwnerDocument(data.optionsElement)
|
let ownerDocument = useOwnerDocument(data.optionsElement)
|
||||||
|
|
||||||
let usesOpenClosedState = useOpenClosed()
|
let usesOpenClosedState = useOpenClosed()
|
||||||
@@ -1163,7 +1164,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
|
|||||||
let render = useRender()
|
let render = useRender()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal enabled={portal ? props.static || visible : false}>
|
<Portal enabled={portal ? props.static || visible : false} ownerDocument={portalOwnerDocument}>
|
||||||
<ListboxDataContext.Provider
|
<ListboxDataContext.Provider
|
||||||
value={data.mode === ValueMode.Multi ? data : { ...data, isSelected }}
|
value={data.mode === ValueMode.Multi ? data : { ...data, isSelected }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -638,6 +638,7 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
|
|||||||
useEvent((element) => dispatch({ type: ActionTypes.SetItemsElement, element })),
|
useEvent((element) => dispatch({ type: ActionTypes.SetItemsElement, element })),
|
||||||
setLocalItemsElement
|
setLocalItemsElement
|
||||||
)
|
)
|
||||||
|
let portalOwnerDocument = useOwnerDocument(state.buttonElement)
|
||||||
let ownerDocument = useOwnerDocument(state.itemsElement)
|
let ownerDocument = useOwnerDocument(state.itemsElement)
|
||||||
|
|
||||||
// Always enable `portal` functionality, when `anchor` is enabled
|
// Always enable `portal` functionality, when `anchor` is enabled
|
||||||
@@ -824,7 +825,7 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
|
|||||||
let render = useRender()
|
let render = useRender()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal enabled={portal ? props.static || visible : false}>
|
<Portal enabled={portal ? props.static || visible : false} ownerDocument={portalOwnerDocument}>
|
||||||
{render({
|
{render({
|
||||||
ourProps,
|
ourProps,
|
||||||
theirProps,
|
theirProps,
|
||||||
|
|||||||
@@ -888,6 +888,7 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
|
|||||||
useEvent((panel) => dispatch({ type: ActionTypes.SetPanel, panel })),
|
useEvent((panel) => dispatch({ type: ActionTypes.SetPanel, panel })),
|
||||||
setLocalPanelElement
|
setLocalPanelElement
|
||||||
)
|
)
|
||||||
|
let portalOwnerDocument = useOwnerDocument(state.button)
|
||||||
let ownerDocument = useOwnerDocument(internalPanelRef)
|
let ownerDocument = useOwnerDocument(internalPanelRef)
|
||||||
|
|
||||||
useIsoMorphicEffect(() => {
|
useIsoMorphicEffect(() => {
|
||||||
@@ -1080,7 +1081,10 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
|
|||||||
<ResetOpenClosedProvider>
|
<ResetOpenClosedProvider>
|
||||||
<PopoverPanelContext.Provider value={id}>
|
<PopoverPanelContext.Provider value={id}>
|
||||||
<PopoverAPIContext.Provider value={{ close, isPortalled }}>
|
<PopoverAPIContext.Provider value={{ close, isPortalled }}>
|
||||||
<Portal enabled={portal ? props.static || visible : false}>
|
<Portal
|
||||||
|
enabled={portal ? props.static || visible : false}
|
||||||
|
ownerDocument={portalOwnerDocument}
|
||||||
|
>
|
||||||
{visible && isPortalled && (
|
{visible && isPortalled && (
|
||||||
<Hidden
|
<Hidden
|
||||||
id={beforePanelSentinelId}
|
id={beforePanelSentinelId}
|
||||||
|
|||||||
@@ -25,12 +25,10 @@ import type { Props } from '../../types'
|
|||||||
import { env } from '../../utils/env'
|
import { env } from '../../utils/env'
|
||||||
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
|
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
|
||||||
|
|
||||||
function usePortalTarget(ref: MutableRefObject<HTMLElement | null>): HTMLElement | null {
|
function usePortalTarget(ownerDocument: Document | null): HTMLElement | null {
|
||||||
let forceInRoot = usePortalRoot()
|
let forceInRoot = usePortalRoot()
|
||||||
let groupTarget = useContext(PortalGroupContext)
|
let groupTarget = useContext(PortalGroupContext)
|
||||||
|
|
||||||
let ownerDocument = useOwnerDocument(ref)
|
|
||||||
|
|
||||||
let [target, setTarget] = useState(() => {
|
let [target, setTarget] = useState(() => {
|
||||||
// Group context is used, but still null
|
// Group context is used, but still null
|
||||||
if (!forceInRoot && groupTarget !== null) return groupTarget.current ?? null
|
if (!forceInRoot && groupTarget !== null) return groupTarget.current ?? null
|
||||||
@@ -77,13 +75,14 @@ export type PortalProps<TTag extends ElementType = typeof DEFAULT_PORTAL_TAG> =
|
|||||||
PortalPropsWeControl,
|
PortalPropsWeControl,
|
||||||
{
|
{
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
|
ownerDocument?: Document | null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
let InternalPortalFn = forwardRefWithAs(function InternalPortalFn<
|
let InternalPortalFn = forwardRefWithAs(function InternalPortalFn<
|
||||||
TTag extends ElementType = typeof DEFAULT_PORTAL_TAG,
|
TTag extends ElementType = typeof DEFAULT_PORTAL_TAG,
|
||||||
>(props: PortalProps<TTag>, ref: Ref<HTMLElement>) {
|
>(props: PortalProps<TTag>, ref: Ref<HTMLElement>) {
|
||||||
let theirProps = props
|
let { ownerDocument: incomingOwnerDocument = null, ...theirProps } = props
|
||||||
let internalPortalRootRef = useRef<HTMLElement | null>(null)
|
let internalPortalRootRef = useRef<HTMLElement | null>(null)
|
||||||
let portalRef = useSyncRefs(
|
let portalRef = useSyncRefs(
|
||||||
optionalRef<(typeof internalPortalRootRef)['current']>((ref) => {
|
optionalRef<(typeof internalPortalRootRef)['current']>((ref) => {
|
||||||
@@ -91,8 +90,9 @@ let InternalPortalFn = forwardRefWithAs(function InternalPortalFn<
|
|||||||
}),
|
}),
|
||||||
ref
|
ref
|
||||||
)
|
)
|
||||||
let ownerDocument = useOwnerDocument(internalPortalRootRef)
|
let defaultOwnerDocument = useOwnerDocument(internalPortalRootRef)
|
||||||
let target = usePortalTarget(internalPortalRootRef)
|
let ownerDocument = incomingOwnerDocument ?? defaultOwnerDocument
|
||||||
|
let target = usePortalTarget(ownerDocument)
|
||||||
let [element] = useState<HTMLDivElement | null>(() =>
|
let [element] = useState<HTMLDivElement | null>(() =>
|
||||||
env.isServer ? null : ownerDocument?.createElement('div') ?? null
|
env.isServer ? null : ownerDocument?.createElement('div') ?? null
|
||||||
)
|
)
|
||||||
@@ -154,12 +154,12 @@ function PortalFn<TTag extends ElementType = typeof DEFAULT_PORTAL_TAG>(
|
|||||||
) {
|
) {
|
||||||
let portalRef = useSyncRefs(ref)
|
let portalRef = useSyncRefs(ref)
|
||||||
|
|
||||||
let { enabled = true, ...theirProps } = props
|
let { enabled = true, ownerDocument, ...theirProps } = props
|
||||||
|
|
||||||
let render = useRender()
|
let render = useRender()
|
||||||
|
|
||||||
return enabled ? (
|
return enabled ? (
|
||||||
<InternalPortalFn {...theirProps} ref={portalRef} />
|
<InternalPortalFn {...theirProps} ownerDocument={ownerDocument} ref={portalRef} />
|
||||||
) : (
|
) : (
|
||||||
render({
|
render({
|
||||||
ourProps: { ref: portalRef },
|
ourProps: { ref: portalRef },
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import { env } from './env'
|
|||||||
|
|
||||||
export function getOwnerDocument<T extends Element | MutableRefObject<Element | null>>(
|
export function getOwnerDocument<T extends Element | MutableRefObject<Element | null>>(
|
||||||
element: T | null | undefined
|
element: T | null | undefined
|
||||||
) {
|
): Document | null {
|
||||||
if (env.isServer) return null
|
if (env.isServer) return null
|
||||||
if (element instanceof Node) return element.ownerDocument
|
if (!element) return document
|
||||||
if (element?.hasOwnProperty('current')) {
|
if ('ownerDocument' in element) return element.ownerDocument
|
||||||
if (element.current instanceof Node) return element.current.ownerDocument
|
if ('current' in element) return element.current?.ownerDocument ?? document
|
||||||
}
|
|
||||||
|
|
||||||
return document
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ import type { Ref } from 'vue'
|
|||||||
import { dom } from './dom'
|
import { dom } from './dom'
|
||||||
import { env } from './env'
|
import { env } from './env'
|
||||||
|
|
||||||
export function getOwnerDocument<T extends HTMLElement | Ref<HTMLElement | null>>(
|
export function getOwnerDocument<T extends Element | Ref<Element | null>>(
|
||||||
element: T | null | undefined
|
element: T | null | undefined
|
||||||
) {
|
): Document | null {
|
||||||
if (env.isServer) return null
|
if (env.isServer) return null
|
||||||
if (element instanceof Node) return element.ownerDocument
|
if (!element) return document
|
||||||
if (element?.hasOwnProperty('value')) {
|
if ('ownerDocument' in element) return element.ownerDocument
|
||||||
let domElement = dom(element as any)
|
if ('value' in element) return dom(element as any)?.ownerDocument ?? document
|
||||||
if (domElement) return domElement.ownerDocument
|
|
||||||
}
|
|
||||||
|
|
||||||
return document
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user