Skip to content

Commit

Permalink
use ownerDocument instead of document
Browse files Browse the repository at this point in the history
This should ensure that in iframes and new windows the correct document
is being used.
  • Loading branch information
RobinMalfait committed Mar 10, 2022
1 parent 07c3a61 commit ac4f6e6
Show file tree
Hide file tree
Showing 27 changed files with 449 additions and 161 deletions.
21 changes: 14 additions & 7 deletions packages/@headlessui-react/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { useOpenClosed, State } from '../../internal/open-closed'
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
import { StackProvider, StackMessage } from '../../internal/stack-context'
import { useOutsideClick } from '../../hooks/use-outside-click'
import { getOwnerDocument } from '../../utils/owner'

enum DialogStates {
Open,
Expand Down Expand Up @@ -231,17 +232,23 @@ let DialogRoot = forwardRefWithAs(function Dialog<
if (dialogState !== DialogStates.Open) return
if (hasParentDialog) return

let overflow = document.documentElement.style.overflow
let paddingRight = document.documentElement.style.paddingRight
let ownerDocument = getOwnerDocument(internalDialogRef)
if (!ownerDocument) return

let scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
let documentElement = ownerDocument.documentElement
let ownerWindow = ownerDocument.defaultView ?? window

document.documentElement.style.overflow = 'hidden'
document.documentElement.style.paddingRight = `${scrollbarWidth}px`
let overflow = documentElement.style.overflow
let paddingRight = documentElement.style.paddingRight

let scrollbarWidth = ownerWindow.innerWidth - documentElement.clientWidth

documentElement.style.overflow = 'hidden'
documentElement.style.paddingRight = `${scrollbarWidth}px`

return () => {
document.documentElement.style.overflow = overflow
document.documentElement.style.paddingRight = paddingRight
documentElement.style.overflow = overflow
documentElement.style.paddingRight = paddingRight
}
}, [dialogState, hasParentDialog])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ import React, {
import { Props } from '../../types'
import { match } from '../../utils/match'
import { forwardRefWithAs, render, Features, PropsForFeatures } from '../../utils/render'
import { useSyncRefs } from '../../hooks/use-sync-refs'
import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
import { useId } from '../../hooks/use-id'
import { Keys } from '../keyboard'
import { isDisabledReactIssue7711 } from '../../utils/bugs'
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { getOwnerDocument } from '../../utils/owner'

enum DisclosureStates {
Open,
Expand Down Expand Up @@ -158,7 +159,18 @@ let DisclosureRoot = forwardRefWithAs(function Disclosure<
let { defaultOpen = false, ...passthroughProps } = props
let buttonId = `headlessui-disclosure-button-${useId()}`
let panelId = `headlessui-disclosure-panel-${useId()}`
let disclosureRef = useSyncRefs(ref)
let internalDisclosureRef = useRef<HTMLElement | null>(null)
let disclosureRef = useSyncRefs(
ref,
optionalRef(
(ref) => {
internalDisclosureRef.current = ref as unknown as HTMLElement | null
},
props.as === undefined ||
// @ts-expect-error The `as` prop _can_ be a Fragment
props.as === React.Fragment
)
)

let panelRef = useRef<StateDefinition['panelRef']['current']>(null)
let buttonRef = useRef<StateDefinition['buttonRef']['current']>(null)
Expand All @@ -179,13 +191,15 @@ let DisclosureRoot = forwardRefWithAs(function Disclosure<
let close = useCallback(
(focusableElement?: HTMLElement | MutableRefObject<HTMLElement | null>) => {
dispatch({ type: ActionTypes.CloseDisclosure })
let ownerDocument = getOwnerDocument(internalDisclosureRef)
if (!ownerDocument) return

let restoreElement = (() => {
if (!focusableElement) return document.getElementById(buttonId)
if (!focusableElement) return ownerDocument.getElementById(buttonId)
if (focusableElement instanceof HTMLElement) return focusableElement
if (focusableElement.current instanceof HTMLElement) return focusableElement.current

return document.getElementById(buttonId)
return ownerDocument.getElementById(buttonId)
})()

restoreElement?.focus()
Expand Down
5 changes: 3 additions & 2 deletions packages/@headlessui-react/src/components/listbox/listbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { useOutsideClick } from '../../hooks/use-outside-click'
import { VisuallyHidden } from '../../internal/visually-hidden'
import { objectToFormEntries } from '../../utils/form'
import { getOwnerDocument } from '../../utils/owner'

enum ListboxStates {
Open,
Expand Down Expand Up @@ -559,7 +560,7 @@ let Options = forwardRefWithAs(function Options<
let container = state.optionsRef.current
if (!container) return
if (state.listboxState !== ListboxStates.Open) return
if (container === document.activeElement) return
if (container === getOwnerDocument(container)?.activeElement) return

container.focus({ preventScroll: true })
}, [state.listboxState, state.optionsRef])
Expand Down Expand Up @@ -704,7 +705,7 @@ let Option = forwardRefWithAs(function Option<
let active =
state.activeOptionIndex !== null ? state.options[state.activeOptionIndex].id === id : false
let selected = state.propsRef.current.value === value
let internalOptionRef = useRef<HTMLElement | null>(null)
let internalOptionRef = useRef<HTMLLIElement | null>(null)
let optionRef = useSyncRefs(ref, internalOptionRef)

useIsoMorphicEffect(() => {
Expand Down
8 changes: 5 additions & 3 deletions packages/@headlessui-react/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { useOutsideClick } from '../../hooks/use-outside-click'
import { useTreeWalker } from '../../hooks/use-tree-walker'
import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-closed'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { useOwnerDocument } from '../../hooks/use-owner'

enum MenuStates {
Open,
Expand Down Expand Up @@ -398,6 +399,7 @@ let Items = forwardRefWithAs(function Items<TTag extends ElementType = typeof DE
) {
let [state, dispatch] = useMenuContext('Menu.Items')
let itemsRef = useSyncRefs(state.itemsRef, ref)
let ownerDocument = useOwnerDocument(state.itemsRef)

let id = `headlessui-menu-items-${useId()}`
let searchDisposables = useDisposables()
Expand All @@ -415,10 +417,10 @@ let Items = forwardRefWithAs(function Items<TTag extends ElementType = typeof DE
let container = state.itemsRef.current
if (!container) return
if (state.menuState !== MenuStates.Open) return
if (container === document.activeElement) return
if (container === ownerDocument?.activeElement) return

container.focus({ preventScroll: true })
}, [state.menuState, state.itemsRef])
}, [state.menuState, state.itemsRef, ownerDocument])

useTreeWalker({
container: state.itemsRef.current,
Expand Down Expand Up @@ -501,7 +503,7 @@ let Items = forwardRefWithAs(function Items<TTag extends ElementType = typeof DE
break
}
},
[dispatch, searchDisposables, state]
[dispatch, searchDisposables, state, ownerDocument]
)

let handleKeyUp = useCallback((event: ReactKeyboardEvent<HTMLButtonElement>) => {
Expand Down
61 changes: 42 additions & 19 deletions packages/@headlessui-react/src/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import { useWindowEvent } from '../../hooks/use-window-event'
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
import { useOutsideClick } from '../../hooks/use-outside-click'
import { getOwnerDocument } from '../../utils/owner'
import { useOwnerDocument } from '../../hooks/use-owner'

enum PopoverStates {
Open,
Expand Down Expand Up @@ -174,7 +176,9 @@ let PopoverRoot = forwardRefWithAs(function Popover<
>(props: Props<TTag, PopoverRenderPropArg>, ref: Ref<HTMLElement>) {
let buttonId = `headlessui-popover-button-${useId()}`
let panelId = `headlessui-popover-panel-${useId()}`
let popoverRef = useSyncRefs(ref)
let internalPopoverRef = useRef<HTMLElement | null>(null)
let popoverRef = useSyncRefs(ref, internalPopoverRef)
let ownerDocument = useOwnerDocument(internalPopoverRef)

let reducerBag = useReducer(stateReducer, {
popoverState: PopoverStates.Closed,
Expand All @@ -198,7 +202,9 @@ let PopoverRoot = forwardRefWithAs(function Popover<
let isFocusWithinPopoverGroup = useCallback(() => {
return (
groupContext?.isFocusWithinPopoverGroup() ??
(button?.contains(document.activeElement) || panel?.contains(document.activeElement))
(ownerDocument?.activeElement &&
(button?.contains(ownerDocument.activeElement) ||
panel?.contains(ownerDocument.activeElement)))
)
}, [groupContext, button, panel])

Expand Down Expand Up @@ -308,17 +314,16 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
isWithinPanel ? null : (button) => dispatch({ type: ActionTypes.SetButton, button })
)
let withinPanelButtonRef = useSyncRefs(internalButtonRef, ref)
let ownerDocument = useOwnerDocument(internalButtonRef)

// TODO: Revisit when handling Tab/Shift+Tab when using Portal's
let activeElementRef = useRef<Element | null>(null)
let previousActiveElementRef = useRef<Element | null>(
typeof window === 'undefined' ? null : document.activeElement
)
let previousActiveElementRef = useRef<Element | null>(null)
useWindowEvent(
'focus',
() => {
previousActiveElementRef.current = activeElementRef.current
activeElementRef.current = document.activeElement
activeElementRef.current = ownerDocument?.activeElement as HTMLElement
},
true
)
Expand Down Expand Up @@ -350,7 +355,12 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
case Keys.Escape:
if (state.popoverState !== PopoverStates.Open) return closeOthers?.(state.buttonId)
if (!internalButtonRef.current) return
if (!internalButtonRef.current.contains(document.activeElement)) return
if (
ownerDocument?.activeElement &&
!internalButtonRef.current.contains(ownerDocument.activeElement)
) {
return
}
event.preventDefault()
event.stopPropagation()
dispatch({ type: ActionTypes.ClosePopover })
Expand All @@ -369,7 +379,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
if (state.panel.contains(previousActiveElementRef.current)) return

// Check if the last focused element is *after* the button in the DOM
let focusableElements = getFocusableElements()
let focusableElements = getFocusableElements(ownerDocument?.body)
let previousIdx = focusableElements.indexOf(
previousActiveElementRef.current as HTMLElement
)
Expand Down Expand Up @@ -425,7 +435,7 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
if (state.panel.contains(previousActiveElementRef.current)) return

// Check if the last focused element is *after* the button in the DOM
let focusableElements = getFocusableElements()
let focusableElements = getFocusableElements(ownerDocument?.body)
let previousIdx = focusableElements.indexOf(
previousActiveElementRef.current as HTMLElement
)
Expand Down Expand Up @@ -586,6 +596,7 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
let panelRef = useSyncRefs(internalPanelRef, ref, (panel) => {
dispatch({ type: ActionTypes.SetPanel, panel })
})
let ownerDocument = useOwnerDocument(internalPanelRef)

let usesOpenClosedState = useOpenClosed()
let visible = (() => {
Expand All @@ -602,7 +613,12 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
case Keys.Escape:
if (state.popoverState !== PopoverStates.Open) return
if (!internalPanelRef.current) return
if (!internalPanelRef.current.contains(document.activeElement)) return
if (
ownerDocument?.activeElement &&
!internalPanelRef.current.contains(ownerDocument.activeElement)
) {
return
}
event.preventDefault()
event.stopPropagation()
dispatch({ type: ActionTypes.ClosePopover })
Expand Down Expand Up @@ -631,7 +647,7 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
if (state.popoverState !== PopoverStates.Open) return
if (!internalPanelRef.current) return

let activeElement = document.activeElement as HTMLElement
let activeElement = ownerDocument?.activeElement as HTMLElement
if (internalPanelRef.current.contains(activeElement)) return // Already focused within Dialog

focusIn(internalPanelRef.current, Focus.First)
Expand All @@ -642,9 +658,9 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
if (state.popoverState !== PopoverStates.Open) return
if (!internalPanelRef.current) return
if (event.key !== Keys.Tab) return
if (!document.activeElement) return
if (!ownerDocument?.activeElement) return
if (!internalPanelRef.current) return
if (!internalPanelRef.current.contains(document.activeElement)) return
if (!internalPanelRef.current.contains(ownerDocument.activeElement)) return

// We will take-over the default tab behaviour so that we have a bit
// control over what is focused next. It will behave exactly the same,
Expand All @@ -659,7 +675,7 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
} else if (result === FocusResult.Overflow) {
if (!state.button) return

let elements = getFocusableElements()
let elements = getFocusableElements(ownerDocument.body)
let buttonIdx = elements.indexOf(state.button)

let nextElements = elements
Expand All @@ -672,7 +688,7 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
// focusable). Therefore we will try and focus the very first item in
// the document.body.
if (focusIn(nextElements, Focus.First) === FocusResult.Error) {
focusIn(document.body, Focus.First)
focusIn(ownerDocument.body, Focus.First)
}
}
})
Expand All @@ -685,7 +701,12 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
if (state.popoverState !== PopoverStates.Open) return
if (!internalPanelRef.current) return

if (internalPanelRef.current?.contains(document.activeElement as HTMLElement)) return
if (
ownerDocument?.activeElement &&
internalPanelRef.current?.contains(ownerDocument.activeElement)
) {
return
}
dispatch({ type: ActionTypes.ClosePopover })
},
true
Expand Down Expand Up @@ -753,15 +774,17 @@ let Group = forwardRefWithAs(function Group<TTag extends ElementType = typeof DE
)

let isFocusWithinPopoverGroup = useCallback(() => {
let element = document.activeElement as HTMLElement
let ownerDocument = getOwnerDocument(internalGroupRef)
if (!ownerDocument) return false
let element = ownerDocument.activeElement

if (internalGroupRef.current?.contains(element)) return true

// Check if the focus is in one of the button or panel elements. This is important in case you are rendering inside a Portal.
return popovers.some((bag) => {
return (
document.getElementById(bag.buttonId)?.contains(element) ||
document.getElementById(bag.panelId)?.contains(element)
ownerDocument!.getElementById(bag.buttonId)?.contains(element) ||
ownerDocument!.getElementById(bag.panelId)?.contains(element)
)
})
}, [internalGroupRef, popovers])
Expand Down
Loading

0 comments on commit ac4f6e6

Please sign in to comment.