diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index 5040eb6481..d7a1f4c66e 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -33,6 +33,7 @@ import { isFocusableElement, FocusableMode } from '../../utils/focus-management' import { useWindowEvent } from '../../hooks/use-window-event' import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-closed' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' +import { getOwnerDocument } from '../../utils/owner-document' enum ListboxStates { Open, @@ -498,7 +499,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]) diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index a664f7be34..a712915f31 100644 --- a/packages/@headlessui-react/src/components/menu/menu.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.tsx @@ -35,6 +35,7 @@ import { useWindowEvent } from '../../hooks/use-window-event' import { useTreeWalker } from '../../hooks/use-tree-walker' import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-closed' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' +import { getOwnerDocument } from '../../utils/owner-document' enum MenuStates { Open, @@ -382,7 +383,7 @@ let Items = forwardRefWithAs(function Items { return ( groupContext?.isFocusWithinPopoverGroup() ?? - (button?.contains(document.activeElement) || panel?.contains(document.activeElement)) + (button?.contains(getOwnerDocument(button).activeElement) || + panel?.contains(getOwnerDocument(panel).activeElement)) ) }, [groupContext, button, panel]) @@ -322,7 +324,7 @@ let Button = forwardRefWithAs(function Button { previousActiveElementRef.current = activeElementRef.current - activeElementRef.current = document.activeElement + activeElementRef.current = getOwnerDocument(internalButtonRef).activeElement as HTMLElement }, true ) @@ -354,7 +356,10 @@ let Button = forwardRefWithAs(function Button { - let element = document.activeElement as HTMLElement + let element = getOwnerDocument(internalGroupRef).activeElement if (internalGroupRef.current?.contains(element)) return true diff --git a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx index f2d0b66430..ee913fa735 100644 --- a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx +++ b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx @@ -26,6 +26,7 @@ import { Label, useLabels } from '../../components/label/label' import { Description, useDescriptions } from '../../components/description/description' import { useTreeWalker } from '../../hooks/use-tree-walker' import { useSyncRefs } from '../../hooks/use-sync-refs' +import { getOwnerDocument } from '../../utils/owner-document' interface Option { id: string @@ -184,7 +185,8 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup< if (result === FocusResult.Success) { let activeOption = options.find( - (option) => option.element.current === document.activeElement + (option) => + option.element.current === getOwnerDocument(option.element).activeElement ) if (activeOption) triggerChange(activeOption.propsRef.current.value) } @@ -201,7 +203,8 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup< if (result === FocusResult.Success) { let activeOption = options.find( - (option) => option.element.current === document.activeElement + (option) => + option.element.current === getOwnerDocument(option.element).activeElement ) if (activeOption) triggerChange(activeOption.propsRef.current.value) } @@ -214,7 +217,7 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup< event.stopPropagation() let activeOption = options.find( - (option) => option.element.current === document.activeElement + (option) => option.element.current === getOwnerDocument(option.element).activeElement ) if (activeOption) triggerChange(activeOption.propsRef.current.value) } diff --git a/packages/@headlessui-react/src/hooks/use-focus-trap.ts b/packages/@headlessui-react/src/hooks/use-focus-trap.ts index fd99685552..d9d0616d0d 100644 --- a/packages/@headlessui-react/src/hooks/use-focus-trap.ts +++ b/packages/@headlessui-react/src/hooks/use-focus-trap.ts @@ -9,6 +9,7 @@ import { Keys } from '../components/keyboard' import { focusElement, focusIn, Focus, FocusResult } from '../utils/focus-management' import { useWindowEvent } from './use-window-event' import { useIsMounted } from './use-is-mounted' +import { getOwnerDocument } from '../utils/owner-document' export enum Features { /** No features enabled for the `useFocusTrap` hook. */ @@ -42,7 +43,9 @@ export function useFocusTrap( } = {} ) { let restoreElement = useRef( - typeof window !== 'undefined' ? (document.activeElement as HTMLElement) : null + typeof window !== 'undefined' + ? (getOwnerDocument(container).activeElement as HTMLElement | null) + : null ) let previousActiveElement = useRef(null) let mounted = useIsMounted() @@ -54,7 +57,7 @@ export function useFocusTrap( useEffect(() => { if (!featuresRestoreFocus) return - restoreElement.current = document.activeElement as HTMLElement + restoreElement.current = getOwnerDocument(container).activeElement as HTMLElement }, [featuresRestoreFocus]) // Restore the focus when we unmount the component. @@ -72,7 +75,7 @@ export function useFocusTrap( if (!featuresInitialFocus) return if (!container.current) return - let activeElement = document.activeElement as HTMLElement + let activeElement = getOwnerDocument(container).activeElement as HTMLElement if (initialFocus?.current) { if (initialFocus?.current === activeElement) { @@ -93,7 +96,7 @@ export function useFocusTrap( } } - previousActiveElement.current = document.activeElement as HTMLElement + previousActiveElement.current = getOwnerDocument(container).activeElement as HTMLElement }, [container, initialFocus, featuresInitialFocus]) // Handle `Tab` & `Shift+Tab` keyboard events @@ -111,7 +114,7 @@ export function useFocusTrap( (event.shiftKey ? Focus.Previous : Focus.Next) | Focus.WrapAround ) === FocusResult.Success ) { - previousActiveElement.current = document.activeElement as HTMLElement + previousActiveElement.current = getOwnerDocument(container).activeElement as HTMLElement } }) diff --git a/packages/@headlessui-react/src/utils/focus-management.ts b/packages/@headlessui-react/src/utils/focus-management.ts index 703e87d5c0..f1b6de537c 100644 --- a/packages/@headlessui-react/src/utils/focus-management.ts +++ b/packages/@headlessui-react/src/utils/focus-management.ts @@ -103,6 +103,12 @@ export function focusElement(element: HTMLElement | null) { } export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) { + let ownerDocument = Array.isArray(container) + ? container.length > 0 + ? container[0].ownerDocument + : document + : container.ownerDocument + let elements = Array.isArray(container) ? container.slice().sort((a, z) => { let position = a.compareDocumentPosition(z) @@ -112,7 +118,7 @@ export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) { return 0 }) : getFocusableElements(container) - let active = document.activeElement as HTMLElement + let active = ownerDocument.activeElement as HTMLElement let direction = (() => { if (focus & (Focus.First | Focus.Next)) return Direction.Next @@ -155,7 +161,7 @@ export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) { // Try the next one in line offset += direction - } while (next !== document.activeElement) + } while (next !== ownerDocument.activeElement) // This is a little weird, but let me try and explain: There are a few scenario's // in chrome for example where a focused `` tag does not get the default focus diff --git a/packages/@headlessui-react/src/utils/owner-document.ts b/packages/@headlessui-react/src/utils/owner-document.ts new file mode 100644 index 0000000000..81694a6205 --- /dev/null +++ b/packages/@headlessui-react/src/utils/owner-document.ts @@ -0,0 +1,14 @@ +import { MutableRefObject } from 'react' + +export function getOwnerDocument>( + element: T | null | undefined +) { + if (element instanceof Element) return element.ownerDocument + if (element && element.hasOwnProperty('current')) { + if (element.current instanceof Element) { + return element.current.ownerDocument + } + } + + return document +} diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts index 1032dade3c..2014e03519 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.ts +++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts @@ -24,6 +24,7 @@ import { useWindowEvent } from '../../hooks/use-window-event' import { useOpenClosed, State, useOpenClosedProvider } from '../../internal/open-closed' import { match } from '../../utils/match' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' +import { getOwnerDocument } from '../../utils/owner-document' enum ListboxStates { Open, @@ -209,7 +210,7 @@ export let Listbox = defineComponent({ useWindowEvent('mousedown', (event) => { let target = event.target as HTMLElement - let active = document.activeElement + let active = getOwnerDocument(event.target as HTMLElement).activeElement if (listboxState.value !== ListboxStates.Open) return if (dom(buttonRef)?.contains(target)) return diff --git a/packages/@headlessui-vue/src/components/menu/menu.ts b/packages/@headlessui-vue/src/components/menu/menu.ts index 7fc505d27e..5a8279e26b 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.ts +++ b/packages/@headlessui-vue/src/components/menu/menu.ts @@ -21,6 +21,7 @@ import { useTreeWalker } from '../../hooks/use-tree-walker' import { useOpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed' import { match } from '../../utils/match' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' +import { getOwnerDocument } from '../../utils/owner-document' enum MenuStates { Open, @@ -162,7 +163,7 @@ export let Menu = defineComponent({ useWindowEvent('mousedown', (event) => { let target = event.target as HTMLElement - let active = document.activeElement + let active = getOwnerDocument(event.target as HTMLElement).activeElement if (menuState.value !== MenuStates.Open) return if (dom(buttonRef)?.contains(target)) return diff --git a/packages/@headlessui-vue/src/components/popover/popover.ts b/packages/@headlessui-vue/src/components/popover/popover.ts index ce2bc125b1..64940a66e0 100644 --- a/packages/@headlessui-vue/src/components/popover/popover.ts +++ b/packages/@headlessui-vue/src/components/popover/popover.ts @@ -28,6 +28,7 @@ import { dom } from '../../utils/dom' import { useWindowEvent } from '../../hooks/use-window-event' import { useOpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' +import { getOwnerDocument } from '../../utils/owner-document' enum PopoverStates { Open, @@ -153,8 +154,8 @@ export let Popover = defineComponent({ function isFocusWithinPopoverGroup() { return ( groupContext?.isFocusWithinPopoverGroup() ?? - (dom(button)?.contains(document.activeElement) || - dom(panel)?.contains(document.activeElement)) + (dom(button)?.contains(getOwnerDocument(button).activeElement) || + dom(panel)?.contains(getOwnerDocument(panel).activeElement)) ) } @@ -225,7 +226,7 @@ export let PopoverButton = defineComponent({ 'focus', () => { previousActiveElementRef.value = activeElementRef.value - activeElementRef.value = document.activeElement + activeElementRef.value = getOwnerDocument(api.button).activeElement }, true ) @@ -269,7 +270,7 @@ export let PopoverButton = defineComponent({ case Keys.Escape: if (api.popoverState.value !== PopoverStates.Open) return closeOthers?.(api.buttonId) if (!dom(api.button)) return - if (!dom(api.button)?.contains(document.activeElement)) return + if (!dom(api.button)?.contains(getOwnerDocument(api.button).activeElement)) return event.preventDefault() event.stopPropagation() api.closePopover() @@ -463,7 +464,7 @@ export let PopoverPanel = defineComponent({ if (api.popoverState.value !== PopoverStates.Open) return if (!api.panel) return - let activeElement = document.activeElement as HTMLElement + let activeElement = getOwnerDocument(api.panel).activeElement as HTMLElement if (dom(api.panel)?.contains(activeElement)) return // Already focused within Dialog focusIn(dom(api.panel)!, Focus.First) @@ -475,8 +476,8 @@ export let PopoverPanel = defineComponent({ if (!dom(api.panel)) return if (event.key !== Keys.Tab) return - if (!document.activeElement) return - if (!dom(api.panel)?.contains(document.activeElement)) return + if (!getOwnerDocument(api.panel).activeElement) return + if (!dom(api.panel)?.contains(getOwnerDocument(api.panel).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, @@ -516,7 +517,8 @@ export let PopoverPanel = defineComponent({ if (!focus) return if (api.popoverState.value !== PopoverStates.Open) return if (!dom(api.panel)) return - if (dom(api.panel)?.contains(document.activeElement as HTMLElement)) return + if (dom(api.panel)?.contains(getOwnerDocument(api.panel).activeElement as HTMLElement)) + return api.closePopover() }, true @@ -536,7 +538,7 @@ export let PopoverPanel = defineComponent({ case Keys.Escape: if (api.popoverState.value !== PopoverStates.Open) return if (!dom(api.panel)) return - if (!dom(api.panel)?.contains(document.activeElement)) return + if (!dom(api.panel)?.contains(getOwnerDocument(api.panel).activeElement)) return event.preventDefault() event.stopPropagation() api.closePopover() @@ -594,7 +596,7 @@ export let PopoverGroup = defineComponent({ } function isFocusWithinPopoverGroup() { - let element = document.activeElement as HTMLElement + let element = getOwnerDocument(groupRef).activeElement as HTMLElement if (dom(groupRef)?.contains(element)) return true diff --git a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts index 44481eea3f..bca68e9c6d 100644 --- a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts +++ b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts @@ -21,6 +21,7 @@ import { omit, render } from '../../utils/render' import { Label, useLabels } from '../label/label' import { Description, useDescriptions } from '../description/description' import { useTreeWalker } from '../../hooks/use-tree-walker' +import { getOwnerDocument } from '../../utils/owner-document' interface Option { id: string @@ -149,7 +150,7 @@ export let RadioGroup = defineComponent({ if (result === FocusResult.Success) { let activeOption = options.value.find( - (option) => option.element === document.activeElement + (option) => option.element === getOwnerDocument(radioGroupRef).activeElement ) if (activeOption) api.change(activeOption.propsRef.value) } @@ -166,7 +167,7 @@ export let RadioGroup = defineComponent({ if (result === FocusResult.Success) { let activeOption = options.value.find( - (option) => option.element === document.activeElement + (option) => option.element === getOwnerDocument(option.element).activeElement ) if (activeOption) api.change(activeOption.propsRef.value) } @@ -179,7 +180,7 @@ export let RadioGroup = defineComponent({ event.stopPropagation() let activeOption = options.value.find( - (option) => option.element === document.activeElement + (option) => option.element === getOwnerDocument(option.element).activeElement ) if (activeOption) api.change(activeOption.propsRef.value) } diff --git a/packages/@headlessui-vue/src/hooks/use-focus-trap.ts b/packages/@headlessui-vue/src/hooks/use-focus-trap.ts index e1f92a8679..b39bb9c28f 100644 --- a/packages/@headlessui-vue/src/hooks/use-focus-trap.ts +++ b/packages/@headlessui-vue/src/hooks/use-focus-trap.ts @@ -12,6 +12,7 @@ import { Keys } from '../keyboard' import { focusElement, focusIn, Focus, FocusResult } from '../utils/focus-management' import { useWindowEvent } from '../hooks/use-window-event' import { contains } from '../internal/dom-containers' +import { getOwnerDocument } from '../utils/owner-document' export function useFocusTrap( containers: Ref>, @@ -26,7 +27,9 @@ export function useFocusTrap( if (containers.value.size !== 1) return let { initialFocus } = options.value - let activeElement = document.activeElement as HTMLElement + let container = Array.from(containers.value)[0] + let containerOwner = getOwnerDocument(container) + let activeElement = containerOwner.activeElement as HTMLElement if (initialFocus) { if (initialFocus === activeElement) { @@ -54,7 +57,7 @@ export function useFocusTrap( if (!couldFocus) console.warn('There are no focusable elements inside the ') } - previousActiveElement.value = document.activeElement as HTMLElement + previousActiveElement.value = containerOwner.activeElement as HTMLElement } // Restore when `enabled` becomes false @@ -76,7 +79,7 @@ export function useFocusTrap( useWindowEvent('keydown', (event) => { if (!enabled.value) return if (event.key !== Keys.Tab) return - if (!document.activeElement) return + if (!getOwnerDocument(event.target as HTMLElement).activeElement) return if (containers.value.size !== 1) return event.preventDefault() @@ -88,7 +91,7 @@ export function useFocusTrap( ) if (result === FocusResult.Success) { - previousActiveElement.value = document.activeElement as HTMLElement + previousActiveElement.value = getOwnerDocument(element).activeElement as HTMLElement break } } diff --git a/packages/@headlessui-vue/src/utils/dom.ts b/packages/@headlessui-vue/src/utils/dom.ts index 5d34de0eed..ccf76c4e61 100644 --- a/packages/@headlessui-vue/src/utils/dom.ts +++ b/packages/@headlessui-vue/src/utils/dom.ts @@ -1,8 +1,6 @@ import { Ref, ComponentPublicInstance } from 'vue' -export function dom( - ref?: Ref -): T | null { +export function dom(ref?: Ref): T | null { if (ref == null) return null if (ref.value == null) return null diff --git a/packages/@headlessui-vue/src/utils/focus-management.ts b/packages/@headlessui-vue/src/utils/focus-management.ts index 06ac5bd6d3..d01dbdcb00 100644 --- a/packages/@headlessui-vue/src/utils/focus-management.ts +++ b/packages/@headlessui-vue/src/utils/focus-management.ts @@ -96,6 +96,13 @@ export function focusElement(element: HTMLElement | null) { } export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) { + let ownerDocument = + (Array.isArray(container) + ? container.length > 0 + ? container[0].ownerDocument + : document + : container?.ownerDocument) ?? document + let elements = Array.isArray(container) ? container.slice().sort((a, z) => { let position = a.compareDocumentPosition(z) @@ -105,7 +112,7 @@ export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) { return 0 }) : getFocusableElements(container) - let active = document.activeElement as HTMLElement + let active = ownerDocument.activeElement as HTMLElement let direction = (() => { if (focus & (Focus.First | Focus.Next)) return Direction.Next @@ -148,7 +155,7 @@ export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) { // Try the next one in line offset += direction - } while (next !== document.activeElement) + } while (next !== ownerDocument.activeElement) // This is a little weird, but let me try and explain: There are a few scenario's // in chrome for example where a focused `` tag does not get the default focus diff --git a/packages/@headlessui-vue/src/utils/owner-document.ts b/packages/@headlessui-vue/src/utils/owner-document.ts new file mode 100644 index 0000000000..370ee6a0d9 --- /dev/null +++ b/packages/@headlessui-vue/src/utils/owner-document.ts @@ -0,0 +1,16 @@ +import { Ref } from 'vue' +import { dom } from './dom' + +export function getOwnerDocument>( + element: T | null | undefined +) { + if (element instanceof Element) return element.ownerDocument + if (element && element.hasOwnProperty('value')) { + let domElement = dom(element) + if (domElement) { + return domElement.ownerDocument + } + } + + return document +}