diff --git a/packages/@react-aria/focus/src/FocusScope.tsx b/packages/@react-aria/focus/src/FocusScope.tsx index 7029a0cfbf0..75d86227956 100644 --- a/packages/@react-aria/focus/src/FocusScope.tsx +++ b/packages/@react-aria/focus/src/FocusScope.tsx @@ -12,7 +12,7 @@ import {FocusableElement} from '@react-types/shared'; import {focusSafely} from './focusSafely'; -import {getOwnerDocument, useLayoutEffect} from '@react-aria/utils'; +import {getRootNode, useLayoutEffect} from '@react-aria/utils'; import {isElementVisible} from './isElementVisible'; import React, {ReactNode, RefObject, useContext, useEffect, useMemo, useRef} from 'react'; @@ -133,7 +133,7 @@ export function FocusScope(props: FocusScopeProps) { // This needs to be an effect so that activeScope is updated after the FocusScope tree is complete. // It cannot be a useLayoutEffect because the parent of this node hasn't been attached in the tree yet. useEffect(() => { - const activeElement = getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined).activeElement; + const activeElement = getRootNode(scopeRef.current ? scopeRef.current[0] : undefined).activeElement; let scope: TreeNode | null = null; if (isElementInScope(activeElement, scopeRef.current)) { @@ -197,7 +197,7 @@ function createFocusManagerForScope(scopeRef: React.RefObject): Focus focusNext(opts: FocusManagerOptions = {}) { let scope = scopeRef.current!; let {from, tabbable, wrap, accept} = opts; - let node = from || getOwnerDocument(scope[0]).activeElement!; + let node = from || getRootNode(scope[0]).activeElement!; let sentinel = scope[0].previousElementSibling!; let scopeRoot = getScopeRoot(scope); let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope); @@ -215,7 +215,7 @@ function createFocusManagerForScope(scopeRef: React.RefObject): Focus focusPrevious(opts: FocusManagerOptions = {}) { let scope = scopeRef.current!; let {from, tabbable, wrap, accept} = opts; - let node = from || getOwnerDocument(scope[0]).activeElement!; + let node = from || getRootNode(scope[0]).activeElement!; let sentinel = scope[scope.length - 1].nextElementSibling!; let scopeRoot = getScopeRoot(scope); let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope); @@ -310,7 +310,7 @@ function useFocusContainment(scopeRef: RefObject, contain?: boolean) return; } - const ownerDocument = getOwnerDocument(scope ? scope[0] : undefined); + const rootNode = getRootNode(scope ? scope[0] : undefined); // Handle the Tab key to contain focus within the scope let onKeyDown = (e) => { @@ -318,7 +318,7 @@ function useFocusContainment(scopeRef: RefObject, contain?: boolean) return; } - let focusedElement = ownerDocument.activeElement; + let focusedElement = rootNode.activeElement; let scope = scopeRef.current; if (!scope || !isElementInScope(focusedElement, scope)) { return; @@ -368,9 +368,9 @@ function useFocusContainment(scopeRef: RefObject, contain?: boolean) } raf.current = requestAnimationFrame(() => { // Use document.activeElement instead of e.relatedTarget so we can tell if user clicked into iframe - if (ownerDocument.activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(ownerDocument.activeElement, scopeRef)) { + if (rootNode.activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(rootNode.activeElement, scopeRef)) { activeScope = scopeRef; - if (ownerDocument.body.contains(e.target)) { + if (rootNode.body.contains(e.target)) { focusedNode.current = e.target; focusedNode.current?.focus(); } else if (activeScope.current) { @@ -380,13 +380,13 @@ function useFocusContainment(scopeRef: RefObject, contain?: boolean) }); }; - ownerDocument.addEventListener('keydown', onKeyDown, false); - ownerDocument.addEventListener('focusin', onFocus, false); + rootNode.addEventListener('keydown', onKeyDown, false); + rootNode.addEventListener('focusin', onFocus, false); scope?.forEach(element => element.addEventListener('focusin', onFocus, false)); scope?.forEach(element => element.addEventListener('focusout', onBlur, false)); return () => { - ownerDocument.removeEventListener('keydown', onKeyDown, false); - ownerDocument.removeEventListener('focusin', onFocus, false); + rootNode.removeEventListener('keydown', onKeyDown, false); + rootNode.removeEventListener('focusin', onFocus, false); scope?.forEach(element => element.removeEventListener('focusin', onFocus, false)); scope?.forEach(element => element.removeEventListener('focusout', onBlur, false)); }; @@ -407,7 +407,7 @@ function isElementInAnyScope(element: Element) { return isElementInChildScope(element); } -function isElementInScope(element?: Element | null, scope?: Element[] | null) { +function isElementInScope(element?: Element | null | ShadowRoot, scope?: Element[] | null) { if (!element) { return false; } @@ -489,8 +489,8 @@ function useAutoFocus(scopeRef: RefObject, autoFocus?: boolean) { useEffect(() => { if (autoFocusRef.current) { activeScope = scopeRef; - const ownerDocument = getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined); - if (!isElementInScope(ownerDocument.activeElement, activeScope.current) && scopeRef.current) { + const rootNode = getRootNode(scopeRef.current ? scopeRef.current[0] : undefined); + if (!isElementInScope(rootNode.activeElement, activeScope.current) && scopeRef.current) { focusFirstInScope(scopeRef.current); } } @@ -507,7 +507,7 @@ function useActiveScopeTracker(scopeRef: RefObject, restore?: boolean } let scope = scopeRef.current; - const ownerDocument = getOwnerDocument(scope ? scope[0] : undefined); + const rootNode = getRootNode(scope ? scope[0] : undefined); let onFocus = (e) => { let target = e.target as Element; @@ -518,10 +518,10 @@ function useActiveScopeTracker(scopeRef: RefObject, restore?: boolean } }; - ownerDocument.addEventListener('focusin', onFocus, false); + rootNode.addEventListener('focusin', onFocus, false); scope?.forEach(element => element.addEventListener('focusin', onFocus, false)); return () => { - ownerDocument.removeEventListener('focusin', onFocus, false); + rootNode.removeEventListener('focusin', onFocus, false); scope?.forEach(element => element.removeEventListener('focusin', onFocus, false)); }; }, [scopeRef, restore, contain]); @@ -543,13 +543,13 @@ function shouldRestoreFocus(scopeRef: ScopeRef) { function useRestoreFocus(scopeRef: RefObject, restoreFocus?: boolean, contain?: boolean) { // create a ref during render instead of useLayoutEffect so the active element is saved before a child with autoFocus=true mounts. // eslint-disable-next-line no-restricted-globals - const nodeToRestoreRef = useRef(typeof document !== 'undefined' ? getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined).activeElement as FocusableElement : null); + const nodeToRestoreRef = useRef(typeof document !== 'undefined' ? getRootNode(scopeRef.current ? scopeRef.current[0] : undefined).activeElement as FocusableElement : null); // restoring scopes should all track if they are active regardless of contain, but contain already tracks it plus logic to contain the focus // restoring-non-containing scopes should only care if they become active so they can perform the restore useLayoutEffect(() => { let scope = scopeRef.current; - const ownerDocument = getOwnerDocument(scope ? scope[0] : undefined); + const rootNode = getRootNode(scope ? scope[0] : undefined); if (!restoreFocus || contain) { return; } @@ -558,23 +558,23 @@ function useRestoreFocus(scopeRef: RefObject, restoreFocus?: boolean, // If focusing an element in a child scope of the currently active scope, the child becomes active. // Moving out of the active scope to an ancestor is not allowed. if ((!activeScope || isAncestorScope(activeScope, scopeRef)) && - isElementInScope(ownerDocument.activeElement, scopeRef.current) + isElementInScope(rootNode.activeElement, scopeRef.current) ) { activeScope = scopeRef; } }; - ownerDocument.addEventListener('focusin', onFocus, false); + rootNode.addEventListener('focusin', onFocus, false); scope?.forEach(element => element.addEventListener('focusin', onFocus, false)); return () => { - ownerDocument.removeEventListener('focusin', onFocus, false); + rootNode.removeEventListener('focusin', onFocus, false); scope?.forEach(element => element.removeEventListener('focusin', onFocus, false)); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [scopeRef, contain]); useLayoutEffect(() => { - const ownerDocument = getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined); + const rootNode = getRootNode(scopeRef.current ? scopeRef.current[0] : undefined); if (!restoreFocus) { return; @@ -589,7 +589,7 @@ function useRestoreFocus(scopeRef: RefObject, restoreFocus?: boolean, return; } - let focusedElement = ownerDocument.activeElement as FocusableElement; + let focusedElement = rootNode.activeElement as FocusableElement; if (!isElementInScope(focusedElement, scopeRef.current)) { return; } @@ -600,13 +600,13 @@ function useRestoreFocus(scopeRef: RefObject, restoreFocus?: boolean, let nodeToRestore = treeNode.nodeToRestore; // Create a DOM tree walker that matches all tabbable elements - let walker = getFocusableTreeWalker(ownerDocument.body, {tabbable: true}); + let walker = getFocusableTreeWalker(rootNode.body, {tabbable: true}); // Find the next tabbable element after the currently focused element walker.currentNode = focusedElement; let nextElement = (e.shiftKey ? walker.previousNode() : walker.nextNode()) as FocusableElement; - if (!nodeToRestore || !ownerDocument.body.contains(nodeToRestore) || nodeToRestore === ownerDocument.body) { + if (!nodeToRestore || !rootNode.body.contains(nodeToRestore) || nodeToRestore === rootNode.body) { nodeToRestore = undefined; treeNode.nodeToRestore = undefined; } @@ -639,19 +639,19 @@ function useRestoreFocus(scopeRef: RefObject, restoreFocus?: boolean, }; if (!contain) { - ownerDocument.addEventListener('keydown', onKeyDown, true); + rootNode.addEventListener('keydown', onKeyDown, true); } return () => { if (!contain) { - ownerDocument.removeEventListener('keydown', onKeyDown, true); + rootNode.removeEventListener('keydown', onKeyDown, true); } }; }, [scopeRef, restoreFocus, contain]); // useLayoutEffect instead of useEffect so the active element is saved synchronously instead of asynchronously. useLayoutEffect(() => { - const ownerDocument = getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined); + const rootNode = getRootNode(scopeRef.current ? scopeRef.current[0] : undefined); if (!restoreFocus) { return; @@ -675,15 +675,15 @@ function useRestoreFocus(scopeRef: RefObject, restoreFocus?: boolean, && nodeToRestore && ( // eslint-disable-next-line react-hooks/exhaustive-deps - isElementInScope(ownerDocument.activeElement, scopeRef.current) - || (ownerDocument.activeElement === ownerDocument.body && shouldRestoreFocus(scopeRef)) + isElementInScope(rootNode.activeElement, scopeRef.current) + || (rootNode.activeElement === rootNode.body && shouldRestoreFocus(scopeRef)) ) ) { // freeze the focusScopeTree so it persists after the raf, otherwise during unmount nodes are removed from it let clonedTree = focusScopeTree.clone(); requestAnimationFrame(() => { // Only restore focus if we've lost focus to the body, the alternative is that focus has been purposefully moved elsewhere - if (ownerDocument.activeElement === ownerDocument.body) { + if (rootNode.activeElement === rootNode.body) { // look up the tree starting with our scope to find a nodeToRestore still in the DOM let treeNode = clonedTree.getTreeNode(scopeRef); while (treeNode) { @@ -717,7 +717,9 @@ function useRestoreFocus(scopeRef: RefObject, restoreFocus?: boolean, */ export function getFocusableTreeWalker(root: Element, opts?: FocusManagerOptions, scope?: Element[]) { let selector = opts?.tabbable ? TABBABLE_ELEMENT_SELECTOR : FOCUSABLE_ELEMENT_SELECTOR; - let walker = getOwnerDocument(root).createTreeWalker( + const document = getRootNode(root); + const docWalker = document instanceof ShadowRoot ? document.ownerDocument : document; + let walker = docWalker.createTreeWalker( root, NodeFilter.SHOW_ELEMENT, { @@ -758,7 +760,7 @@ export function createFocusManager(ref: RefObject, defaultOptions: Focu return null; } let {from, tabbable = defaultOptions.tabbable, wrap = defaultOptions.wrap, accept = defaultOptions.accept} = opts; - let node = from || getOwnerDocument(root).activeElement; + let node = from || getRootNode(root).activeElement; let walker = getFocusableTreeWalker(root, {tabbable, accept}); if (root.contains(node)) { walker.currentNode = node!; @@ -779,7 +781,7 @@ export function createFocusManager(ref: RefObject, defaultOptions: Focu return null; } let {from, tabbable = defaultOptions.tabbable, wrap = defaultOptions.wrap, accept = defaultOptions.accept} = opts; - let node = from || getOwnerDocument(root).activeElement; + let node = from || getRootNode(root).activeElement; let walker = getFocusableTreeWalker(root, {tabbable, accept}); if (root.contains(node)) { walker.currentNode = node!; diff --git a/packages/@react-aria/focus/src/focusSafely.ts b/packages/@react-aria/focus/src/focusSafely.ts index e0bc52e9465..6a4dc1bfe3d 100644 --- a/packages/@react-aria/focus/src/focusSafely.ts +++ b/packages/@react-aria/focus/src/focusSafely.ts @@ -11,7 +11,7 @@ */ import {FocusableElement} from '@react-types/shared'; -import {focusWithoutScrolling, getOwnerDocument, runAfterTransition} from '@react-aria/utils'; +import {focusWithoutScrolling, getRootNode, runAfterTransition} from '@react-aria/utils'; import {getInteractionModality} from '@react-aria/interactions'; /** @@ -24,7 +24,7 @@ export function focusSafely(element: FocusableElement) { // the page before shifting focus. This avoids issues with VoiceOver on iOS // causing the page to scroll when moving focus if the element is transitioning // from off the screen. - const ownerDocument = getOwnerDocument(element); + const ownerDocument = getRootNode(element); if (getInteractionModality() === 'virtual') { let lastFocusedElement = ownerDocument.activeElement; runAfterTransition(() => { diff --git a/packages/@react-aria/grid/src/useGridCell.ts b/packages/@react-aria/grid/src/useGridCell.ts index 5efe934992e..e44626ad4e9 100644 --- a/packages/@react-aria/grid/src/useGridCell.ts +++ b/packages/@react-aria/grid/src/useGridCell.ts @@ -12,7 +12,7 @@ import {DOMAttributes, FocusableElement} from '@react-types/shared'; import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus'; -import {getScrollParent, mergeProps, scrollIntoViewport} from '@react-aria/utils'; +import {getDeepActiveElement, getScrollParent, mergeProps, scrollIntoViewport} from '@react-aria/utils'; import {GridCollection, GridNode} from '@react-types/grid'; import {gridMap} from './utils'; import {GridState} from '@react-stately/grid'; @@ -70,9 +70,10 @@ export function useGridCell>(props: GridCellProps // it is focused, otherwise the cell itself is focused. let focus = () => { let treeWalker = getFocusableTreeWalker(ref.current); + const documentActiveElement = getDeepActiveElement(); if (focusMode === 'child') { // If focus is already on a focusable child within the cell, early return so we don't shift focus - if (ref.current.contains(document.activeElement) && ref.current !== document.activeElement) { + if (ref.current.contains(documentActiveElement) && ref.current !== documentActiveElement) { return; } @@ -87,7 +88,7 @@ export function useGridCell>(props: GridCellProps if ( (keyWhenFocused.current != null && node.key !== keyWhenFocused.current) || - !ref.current.contains(document.activeElement) + !ref.current.contains(documentActiveElement) ) { focusSafely(ref.current); } @@ -110,7 +111,7 @@ export function useGridCell>(props: GridCellProps } let walker = getFocusableTreeWalker(ref.current); - walker.currentNode = document.activeElement; + walker.currentNode = getDeepActiveElement(); switch (e.key) { case 'ArrowLeft': { @@ -232,7 +233,7 @@ export function useGridCell>(props: GridCellProps // If the cell itself is focused, wait a frame so that focus finishes propagatating // up to the tree, and move focus to a focusable child if possible. requestAnimationFrame(() => { - if (focusMode === 'child' && document.activeElement === ref.current) { + if (focusMode === 'child' && getDeepActiveElement() === ref.current) { focus(); } }); diff --git a/packages/@react-aria/interactions/src/useFocus.ts b/packages/@react-aria/interactions/src/useFocus.ts index bf877c9988d..5aee99c3211 100644 --- a/packages/@react-aria/interactions/src/useFocus.ts +++ b/packages/@react-aria/interactions/src/useFocus.ts @@ -17,7 +17,7 @@ import {DOMAttributes, FocusableElement, FocusEvents} from '@react-types/shared'; import {FocusEvent, useCallback} from 'react'; -import {getOwnerDocument} from '@react-aria/utils'; +import {getRootNode} from '@react-aria/utils'; import {useSyntheticBlurEvent} from './utils'; export interface FocusProps extends FocusEvents { @@ -63,7 +63,7 @@ export function useFocus(pro // Double check that document.activeElement actually matches e.target in case a previously chained // focus handler already moved focus somewhere else. - const ownerDocument = getOwnerDocument(e.target); + const ownerDocument = getRootNode(e.target); if (e.target === e.currentTarget && ownerDocument.activeElement === e.target) { if (onFocusProp) { diff --git a/packages/@react-aria/interactions/src/useFocusVisible.ts b/packages/@react-aria/interactions/src/useFocusVisible.ts index d0637c913dd..f7d54477121 100644 --- a/packages/@react-aria/interactions/src/useFocusVisible.ts +++ b/packages/@react-aria/interactions/src/useFocusVisible.ts @@ -15,7 +15,7 @@ // NOTICE file in the root directory of this source tree. // See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions -import {getOwnerDocument, getOwnerWindow, isMac, isVirtualClick} from '@react-aria/utils'; +import {getOwnerWindow, getRootNode, isMac, isVirtualClick} from '@react-aria/utils'; import {useEffect, useState} from 'react'; import {useIsSSR} from '@react-aria/ssr'; @@ -123,7 +123,7 @@ function setupGlobalFocusEvents(element?: HTMLElement | null) { } const windowObject = getOwnerWindow(element); - const documentObject = getOwnerDocument(element); + const documentObject = getRootNode(element); // Programmatic focus() calls shouldn't affect the current input modality. // However, we need to detect other cases when a focus event occurs without @@ -164,7 +164,7 @@ function setupGlobalFocusEvents(element?: HTMLElement | null) { const tearDownWindowFocusTracking = (element, loadListener?: () => void) => { const windowObject = getOwnerWindow(element); - const documentObject = getOwnerDocument(element); + const documentObject = getRootNode(element); if (loadListener) { documentObject.removeEventListener('DOMContentLoaded', loadListener); } @@ -210,7 +210,7 @@ const tearDownWindowFocusTracking = (element, loadListener?: () => void) => { * @returns A function to remove the event listeners and cleanup the state. */ export function addWindowFocusTracking(element?: HTMLElement | null): () => void { - const documentObject = getOwnerDocument(element); + const documentObject = getRootNode(element); let loadListener; if (documentObject.readyState !== 'loading') { setupGlobalFocusEvents(element); diff --git a/packages/@react-aria/interactions/src/useInteractOutside.ts b/packages/@react-aria/interactions/src/useInteractOutside.ts index 99625c3bb69..0b7609c67c8 100644 --- a/packages/@react-aria/interactions/src/useInteractOutside.ts +++ b/packages/@react-aria/interactions/src/useInteractOutside.ts @@ -15,7 +15,7 @@ // NOTICE file in the root directory of this source tree. // See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions -import {getOwnerDocument, useEffectEvent} from '@react-aria/utils'; +import {getRootNode, useEffectEvent} from '@react-aria/utils'; import {RefObject, useEffect, useRef} from 'react'; export interface InteractOutsideProps { @@ -59,7 +59,8 @@ export function useInteractOutside(props: InteractOutsideProps) { } const element = ref.current; - const documentObject = getOwnerDocument(element); + const rootNode = getRootNode(element); + const documentObject = rootNode instanceof ShadowRoot ? rootNode.ownerDocument : rootNode; // Use pointer events if available. Otherwise, fall back to mouse and touch events. if (typeof PointerEvent !== 'undefined') { diff --git a/packages/@react-aria/interactions/src/usePress.ts b/packages/@react-aria/interactions/src/usePress.ts index 583a71bfb2f..e81991e7307 100644 --- a/packages/@react-aria/interactions/src/usePress.ts +++ b/packages/@react-aria/interactions/src/usePress.ts @@ -15,7 +15,7 @@ // NOTICE file in the root directory of this source tree. // See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions -import {chain, focusWithoutScrolling, getOwnerDocument, getOwnerWindow, isMac, isVirtualClick, isVirtualPointerEvent, mergeProps, openLink, useEffectEvent, useGlobalListeners, useSyncRef} from '@react-aria/utils'; +import {chain, focusWithoutScrolling, getOwnerWindow, getRootNode, isMac, isVirtualClick, isVirtualPointerEvent, mergeProps, openLink, useEffectEvent, useGlobalListeners, useSyncRef} from '@react-aria/utils'; import {disableTextSelection, restoreTextSelection} from './textSelection'; import {DOMAttributes, FocusableElement, PressEvent as IPressEvent, PointerType, PressEvents} from '@react-types/shared'; import {PressResponderContext} from './context'; @@ -279,7 +279,7 @@ export function usePress(props: PressHookProps): PressResult { } }; - addGlobalListener(getOwnerDocument(e.currentTarget), 'keyup', chain(pressUp, onKeyUp), true); + addGlobalListener(getRootNode(e.currentTarget), 'keyup', chain(pressUp, onKeyUp), true); } if (shouldStopPropagation) { @@ -409,9 +409,9 @@ export function usePress(props: PressHookProps): PressResult { shouldStopPropagation = triggerPressStart(e, state.pointerType); - addGlobalListener(getOwnerDocument(e.currentTarget), 'pointermove', onPointerMove, false); - addGlobalListener(getOwnerDocument(e.currentTarget), 'pointerup', onPointerUp, false); - addGlobalListener(getOwnerDocument(e.currentTarget), 'pointercancel', onPointerCancel, false); + addGlobalListener(getRootNode(e.currentTarget), 'pointermove', onPointerMove, false); + addGlobalListener(getRootNode(e.currentTarget), 'pointerup', onPointerUp, false); + addGlobalListener(getRootNode(e.currentTarget), 'pointercancel', onPointerCancel, false); } if (shouldStopPropagation) { @@ -533,7 +533,7 @@ export function usePress(props: PressHookProps): PressResult { e.stopPropagation(); } - addGlobalListener(getOwnerDocument(e.currentTarget), 'mouseup', onMouseUp, false); + addGlobalListener(getRootNode(e.currentTarget), 'mouseup', onMouseUp, false); }; pressProps.onMouseEnter = (e) => { diff --git a/packages/@react-aria/interactions/stories/useFocusRing.stories.tsx b/packages/@react-aria/interactions/stories/useFocusRing.stories.tsx index 7598fb38ac8..d3885651e89 100644 --- a/packages/@react-aria/interactions/stories/useFocusRing.stories.tsx +++ b/packages/@react-aria/interactions/stories/useFocusRing.stories.tsx @@ -69,6 +69,11 @@ export const IFrame = { name: 'focus state in dynamic iframe' }; +export const ShadowRoot = { + render: () => , + name: 'focus state in shadow root' +}; + function SearchExample() { const [items, setItems] = useState(manyRows); @@ -147,3 +152,48 @@ function IFrameExample() { ); } + +function ShadowDomWrapper({children}) { + const container = useRef(null); + + useEffect(() => { + const _container = container.current; + + if (!_container) {return;} + + const div = document.createElement('div'); + _container.appendChild(div); + + const shadowRoot = div.attachShadow({mode: 'open'}); + const main = document.createElement('main'); + shadowRoot.appendChild(main); + ReactDOM.render(children, main); + + const teardown = addWindowFocusTracking(shadowRoot); + + return () => { + teardown(); + if (_container) { + _container.innerHTML = ''; + } + }; + }, [children]); + + return
; +} + +function ShadowRootExample() { + return ( + <> +
+ + ); +} diff --git a/packages/@react-aria/overlays/src/useOverlay.ts b/packages/@react-aria/overlays/src/useOverlay.ts index 2e4dc24cac4..26ca9067904 100644 --- a/packages/@react-aria/overlays/src/useOverlay.ts +++ b/packages/@react-aria/overlays/src/useOverlay.ts @@ -92,7 +92,8 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject): Ov }; let onInteractOutsideStart = (e: PointerEvent) => { - if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) { + const actualTarget = e.composedPath()[0] as Element; + if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(actualTarget)) { if (visibleOverlays[visibleOverlays.length - 1] === ref) { e.stopPropagation(); e.preventDefault(); @@ -101,7 +102,8 @@ export function useOverlay(props: AriaOverlayProps, ref: RefObject): Ov }; let onInteractOutside = (e: PointerEvent) => { - if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) { + const actualTarget = e.composedPath()[0] as Element; + if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(actualTarget)) { if (visibleOverlays[visibleOverlays.length - 1] === ref) { e.stopPropagation(); e.preventDefault(); diff --git a/packages/@react-aria/utils/src/domHelpers.ts b/packages/@react-aria/utils/src/domHelpers.ts index b8be4d07ee2..3229d988bfd 100644 --- a/packages/@react-aria/utils/src/domHelpers.ts +++ b/packages/@react-aria/utils/src/domHelpers.ts @@ -9,6 +9,26 @@ export const getOwnerWindow = ( return el; } - const doc = getOwnerDocument(el as Element | null | undefined); - return doc.defaultView || window; + const doc = getRootNode(el as Element | null | undefined); + return doc instanceof ShadowRoot ? doc.ownerDocument.defaultView || window : doc.defaultView || window; +}; + +export const getRootNode = (el: Element | null | undefined): Document | (ShadowRoot & { + body: ShadowRoot +}) => { + const rootNode = el?.getRootNode() ?? document; + + if (rootNode instanceof ShadowRoot) { + rootNode.body = rootNode; + } + + return rootNode; +}; + +export const getDeepActiveElement = () => { + let activeElement = document.activeElement; + while (activeElement.shadowRoot && activeElement.shadowRoot.activeElement) { + activeElement = activeElement.shadowRoot.activeElement; + } + return activeElement; }; diff --git a/packages/@react-aria/utils/src/index.ts b/packages/@react-aria/utils/src/index.ts index 0f9f03377df..906ccd12fbb 100644 --- a/packages/@react-aria/utils/src/index.ts +++ b/packages/@react-aria/utils/src/index.ts @@ -11,7 +11,7 @@ */ export {useId, mergeIds, useSlotId} from './useId'; export {chain} from './chain'; -export {getOwnerDocument, getOwnerWindow} from './domHelpers'; +export {getOwnerDocument, getOwnerWindow, getRootNode, getDeepActiveElement} from './domHelpers'; export {mergeProps} from './mergeProps'; export {mergeRefs} from './mergeRefs'; export {filterDOMProps} from './filterDOMProps';