Skip to content

Commit c78b248

Browse files
MahmoudElsayadritz078snowystingerLFDanLu
authored
feat: Focus Management within ShadowDOM (#6046)
* Add `getRootNode` utility. * Update `getRootNode` util. Update domHelpers.test.js. * Update `getOwnerWindow` util. * Add tests for Shadow DOM handling using `getRootNode`. * Update comment. * Fix FocusScope.tsx in Shadow DOM. Add Tests for FocusScope.test.js. New helper util `getRootBody`. * Add more test for FocusScope.test.js. Fix `useRestoreFocus` issue. Add new DOM util `getDeepActiveElement`. * Fix another `useRestoreFocus` issue with restoring focus in `Keyboard navigation example`. * Add tests for `getDeepActiveElement` * Add `useFocus` shadow DOM tests. update `useFocus` - `useFocusWithin` - `usePress`. * Update `focusSafely`. Test for `focusSafely`. * Update `useInteractionOutside` for Shadow DOM support. * Update `useFocusVisible` for Shadow DOM support. * Add `useInteractOutside` tests. * Add test for use case mentioned in issue #1472. * Add tests for `usePress` hook. * Update the fix for `useInteractOutside` to use simpler one. * Update `useOverlay` to use composedPath. * Tests refactor. * Revert `useOverlay` changes as it works correctly without these changes. * Fix types. * Fix types. * lint. * lint. * Fix failing tests. * Fix failing tests. * Fix failing tests. * Test CI * Test CI * Fix shadow DOM tests * Fix shadow DOM tests. * Fix CI? * Fix CI? * Fix CI? * Re-add commented test. * Update `getRootNode` to handle iframes as well, and everything that `getOwnerDocument` used to handle. * Fix tests. * Fix tests? * Fix tests? * Fix tests. * Fix tests.? * Fix tests.? * Fix tests.? * Fix tests.? * Apply suggestions from code review Co-authored-by: Robert Snow <[email protected]> * Update packages/@react-aria/interactions/test/usePress.test.js Co-authored-by: Robert Snow <[email protected]> * - Update tests to use `createShadowRoot` util. - Update `getRootNode` to return null for disconnected nodes. - Update `usePress.test.js` shadow DOM test. - Test getting rid of reactDomRenderer. * - Update tests and remove reactCompat. * - Leftover. * - Revert changes to getFocusableTreeWalker. * - Remove casting. * - return null in case element is disconnected in `getRootNode`. * - Casting. * - Update unit test. * - Handle focus movements between shadow DOMs. * - TS fixes. * Update usePress.test.js * Refactors and TS errors. * Update fix. * Remove broken sandbox link. * Refactor `getRootNode` to improve root node handling. * Use `getDeepActiveElement` inside focusSafely.ts to get the active element. * Refactor event listener registration Introduce `createEventListener` function to streamline event listener registration. This enhances readability and maintainability, ensuring consistency across event handling logic. * Remove `ownerDocument` fallback in usePress.ts * Refactor `createEventListener` for type-safe caching. * - Test out the updated getOwnerWindow to fix iframe focus issues. * - Test out the updated getOwnerWindow to fix iframe focus issues. * - Test? * - Revert Focus scope changes, for testing. * - Fix tests? * - Fix tests? * - Fix tests? * - Revert the changes to getRootNode. * - Revert `isElementInScope` as well. * - Test out if instance check failure across context for iframes is what is causing the issue. * - Replace the use of `instanceof` with `nodeType` to correctly identify the node type across contexts. - Revert changes made for `usePress`. * - Fix ESlint errors. * - Update the usages of `instanceof` to use `nodeType` instead. * Update packages/@react-aria/interactions/src/useFocusVisible.ts Co-authored-by: Robert Snow <[email protected]> * - Update the usages of `instanceof` to use `nodeType` instead. - Introduce new helpers `isShadowRoot` and `isDocument`. * - Lint. * - Lint. * - Update `getDeepActiveElement` to accept an optional document or shadowRoot. - Fix an issue where opening any popover, the focus wasn't restored to the trigger element in shadow DOM. * - Add extra unit test for `getDeepActiveElement`. * - Update `getDeepActiveElement` to always rely on `getRootNode`. * - Update `getDeepActiveElement` to always rely on `getRootNode`. * refactor usePress to still have global listeners for cleanup across boundaries * fix lint and test * restore remaining document level listeners * fix tests * fix lint * simplify * Update packages/@react-aria/focus/src/FocusScope.tsx * fix autofocus * minor test updates to preserve test intent * review comments * fix esm test * fix lint * check in speed tests * fix lint * Add feature flag and fix a couple probable bugs * Update NOTICE.txt --------- Co-authored-by: Ritesh Kumar <[email protected]> Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Daniel Lu <[email protected]>
1 parent 6c4c57d commit c78b248

32 files changed

+2753
-508
lines changed

NOTICE.txt

+29
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,32 @@ This codebase contains a modified portion of code from Yarn berry which can be o
211211
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
212212

213213
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
214+
215+
216+
-------------------------------------------------------------------------------
217+
This codebase contains a modified portion of code from Microsoft which can be obtained at:
218+
* SOURCE:
219+
* https://github.com/microsoft/tabster
220+
221+
* LICENSE:
222+
MIT License
223+
224+
Copyright (c) Microsoft Corporation.
225+
226+
Permission is hereby granted, free of charge, to any person obtaining a copy
227+
of this software and associated documentation files (the "Software"), to deal
228+
in the Software without restriction, including without limitation the rights
229+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
230+
copies of the Software, and to permit persons to whom the Software is
231+
furnished to do so, subject to the following conditions:
232+
233+
The above copyright notice and this permission notice shall be included in all
234+
copies or substantial portions of the Software.
235+
236+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
237+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
238+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
239+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
240+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
241+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
242+
SOFTWARE

packages/@react-aria/focus/src/FocusScope.tsx

+57-34
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,21 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
import {
14+
createShadowTreeWalker,
15+
getActiveElement,
16+
getEventTarget,
17+
getOwnerDocument,
18+
isAndroid,
19+
isChrome,
20+
isFocusable,
21+
isTabbable,
22+
ShadowTreeWalker,
23+
useLayoutEffect
24+
} from '@react-aria/utils';
1325
import {FocusableElement, RefObject} from '@react-types/shared';
1426
import {focusSafely} from './focusSafely';
1527
import {getInteractionModality} from '@react-aria/interactions';
16-
import {getOwnerDocument, isAndroid, isChrome, isFocusable, isTabbable, useLayoutEffect} from '@react-aria/utils';
1728
import {isElementVisible} from './isElementVisible';
1829
import React, {ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
1930

@@ -55,7 +66,7 @@ export interface FocusManager {
5566
focusPrevious(opts?: FocusManagerOptions): FocusableElement | null,
5667
/** Moves focus to the first focusable or tabbable element in the focus scope. */
5768
focusFirst(opts?: FocusManagerOptions): FocusableElement | null,
58-
/** Moves focus to the last focusable or tabbable element in the focus scope. */
69+
/** Moves focus to the last focusable or tabbable element in the focus scope. */
5970
focusLast(opts?: FocusManagerOptions): FocusableElement | null
6071
}
6172

@@ -144,7 +155,7 @@ export function FocusScope(props: FocusScopeProps) {
144155
// This needs to be an effect so that activeScope is updated after the FocusScope tree is complete.
145156
// It cannot be a useLayoutEffect because the parent of this node hasn't been attached in the tree yet.
146157
useEffect(() => {
147-
const activeElement = getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined).activeElement;
158+
const activeElement = getActiveElement(getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined));
148159
let scope: TreeNode | null = null;
149160

150161
if (isElementInScope(activeElement, scopeRef.current)) {
@@ -208,7 +219,7 @@ function createFocusManagerForScope(scopeRef: React.RefObject<Element[] | null>)
208219
focusNext(opts: FocusManagerOptions = {}) {
209220
let scope = scopeRef.current!;
210221
let {from, tabbable, wrap, accept} = opts;
211-
let node = from || getOwnerDocument(scope[0]).activeElement!;
222+
let node = from || getActiveElement(getOwnerDocument(scope[0] ?? undefined))!;
212223
let sentinel = scope[0].previousElementSibling!;
213224
let scopeRoot = getScopeRoot(scope);
214225
let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope);
@@ -226,11 +237,11 @@ function createFocusManagerForScope(scopeRef: React.RefObject<Element[] | null>)
226237
focusPrevious(opts: FocusManagerOptions = {}) {
227238
let scope = scopeRef.current!;
228239
let {from, tabbable, wrap, accept} = opts;
229-
let node = from || getOwnerDocument(scope[0]).activeElement!;
240+
let node = from || getActiveElement(getOwnerDocument(scope[0] ?? undefined))!;
230241
let sentinel = scope[scope.length - 1].nextElementSibling!;
231242
let scopeRoot = getScopeRoot(scope);
232243
let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope);
233-
walker.currentNode = isElementInScope(node, scope) ? node : sentinel;
244+
walker.currentNode = isElementInScope(node, scope) ? node : sentinel;
234245
let previousNode = walker.previousNode() as FocusableElement;
235246
if (!previousNode && wrap) {
236247
walker.currentNode = sentinel;
@@ -308,7 +319,7 @@ function useFocusContainment(scopeRef: RefObject<Element[] | null>, contain?: bo
308319
return;
309320
}
310321

311-
let focusedElement = ownerDocument.activeElement;
322+
let focusedElement = getActiveElement(ownerDocument);
312323
let scope = scopeRef.current;
313324
if (!scope || !isElementInScope(focusedElement, scope)) {
314325
return;
@@ -332,13 +343,13 @@ function useFocusContainment(scopeRef: RefObject<Element[] | null>, contain?: bo
332343
}
333344
};
334345

335-
let onFocus = (e) => {
346+
let onFocus: EventListener = (e) => {
336347
// If focusing an element in a child scope of the currently active scope, the child becomes active.
337348
// Moving out of the active scope to an ancestor is not allowed.
338-
if ((!activeScope || isAncestorScope(activeScope, scopeRef)) && isElementInScope(e.target, scopeRef.current)) {
349+
if ((!activeScope || isAncestorScope(activeScope, scopeRef)) && isElementInScope(getEventTarget(e) as Element, scopeRef.current)) {
339350
activeScope = scopeRef;
340-
focusedNode.current = e.target;
341-
} else if (shouldContainFocus(scopeRef) && !isElementInChildScope(e.target, scopeRef)) {
351+
focusedNode.current = getEventTarget(e) as FocusableElement;
352+
} else if (shouldContainFocus(scopeRef) && !isElementInChildScope(getEventTarget(e) as Element, scopeRef)) {
342353
// If a focus event occurs outside the active scope (e.g. user tabs from browser location bar),
343354
// restore focus to the previously focused node or the first tabbable element in the active scope.
344355
if (focusedNode.current) {
@@ -347,11 +358,11 @@ function useFocusContainment(scopeRef: RefObject<Element[] | null>, contain?: bo
347358
focusFirstInScope(activeScope.current);
348359
}
349360
} else if (shouldContainFocus(scopeRef)) {
350-
focusedNode.current = e.target;
361+
focusedNode.current = getEventTarget(e) as FocusableElement;
351362
}
352363
};
353364

354-
let onBlur = (e) => {
365+
let onBlur: EventListener = (e) => {
355366
// Firefox doesn't shift focus back to the Dialog properly without this
356367
if (raf.current) {
357368
cancelAnimationFrame(raf.current);
@@ -364,10 +375,12 @@ function useFocusContainment(scopeRef: RefObject<Element[] | null>, contain?: bo
364375
let shouldSkipFocusRestore = (modality === 'virtual' || modality === null) && isAndroid() && isChrome();
365376

366377
// Use document.activeElement instead of e.relatedTarget so we can tell if user clicked into iframe
367-
if (!shouldSkipFocusRestore && ownerDocument.activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(ownerDocument.activeElement, scopeRef)) {
378+
let activeElement = getActiveElement(ownerDocument);
379+
if (!shouldSkipFocusRestore && activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(activeElement, scopeRef)) {
368380
activeScope = scopeRef;
369-
if (ownerDocument.body.contains(e.target)) {
370-
focusedNode.current = e.target;
381+
let target = getEventTarget(e) as FocusableElement;
382+
if (target && target.isConnected) {
383+
focusedNode.current = target;
371384
focusedNode.current?.focus();
372385
} else if (activeScope.current) {
373386
focusFirstInScope(activeScope.current);
@@ -490,7 +503,7 @@ function useAutoFocus(scopeRef: RefObject<Element[] | null>, autoFocus?: boolean
490503
if (autoFocusRef.current) {
491504
activeScope = scopeRef;
492505
const ownerDocument = getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined);
493-
if (!isElementInScope(ownerDocument.activeElement, activeScope.current) && scopeRef.current) {
506+
if (!isElementInScope(getActiveElement(ownerDocument), activeScope.current) && scopeRef.current) {
494507
focusFirstInScope(scopeRef.current);
495508
}
496509
}
@@ -510,7 +523,7 @@ function useActiveScopeTracker(scopeRef: RefObject<Element[] | null>, restore?:
510523
const ownerDocument = getOwnerDocument(scope ? scope[0] : undefined);
511524

512525
let onFocus = (e) => {
513-
let target = e.target as Element;
526+
let target = getEventTarget(e) as Element;
514527
if (isElementInScope(target, scopeRef.current)) {
515528
activeScope = scopeRef;
516529
} else if (!isElementInAnyScope(target)) {
@@ -543,7 +556,7 @@ function shouldRestoreFocus(scopeRef: ScopeRef) {
543556
function useRestoreFocus(scopeRef: RefObject<Element[] | null>, restoreFocus?: boolean, contain?: boolean) {
544557
// create a ref during render instead of useLayoutEffect so the active element is saved before a child with autoFocus=true mounts.
545558
// eslint-disable-next-line no-restricted-globals
546-
const nodeToRestoreRef = useRef(typeof document !== 'undefined' ? getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined).activeElement as FocusableElement : null);
559+
const nodeToRestoreRef = useRef(typeof document !== 'undefined' ? getActiveElement(getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined)) as FocusableElement : null);
547560

548561
// restoring scopes should all track if they are active regardless of contain, but contain already tracks it plus logic to contain the focus
549562
// restoring-non-containing scopes should only care if they become active so they can perform the restore
@@ -558,7 +571,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[] | null>, restoreFocus?: b
558571
// If focusing an element in a child scope of the currently active scope, the child becomes active.
559572
// Moving out of the active scope to an ancestor is not allowed.
560573
if ((!activeScope || isAncestorScope(activeScope, scopeRef)) &&
561-
isElementInScope(ownerDocument.activeElement, scopeRef.current)
574+
isElementInScope(getActiveElement(ownerDocument), scopeRef.current)
562575
) {
563576
activeScope = scopeRef;
564577
}
@@ -570,7 +583,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[] | null>, restoreFocus?: b
570583
ownerDocument.removeEventListener('focusin', onFocus, false);
571584
scope?.forEach(element => element.removeEventListener('focusin', onFocus, false));
572585
};
573-
// eslint-disable-next-line react-hooks/exhaustive-deps
586+
// eslint-disable-next-line react-hooks/exhaustive-deps
574587
}, [scopeRef, contain]);
575588

576589
useLayoutEffect(() => {
@@ -606,7 +619,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[] | null>, restoreFocus?: b
606619
walker.currentNode = focusedElement;
607620
let nextElement = (e.shiftKey ? walker.previousNode() : walker.nextNode()) as FocusableElement;
608621

609-
if (!nodeToRestore || !ownerDocument.body.contains(nodeToRestore) || nodeToRestore === ownerDocument.body) {
622+
if (!nodeToRestore || !nodeToRestore.isConnected || nodeToRestore === ownerDocument.body) {
610623
nodeToRestore = undefined;
611624
treeNode.nodeToRestore = undefined;
612625
}
@@ -626,9 +639,9 @@ function useRestoreFocus(scopeRef: RefObject<Element[] | null>, restoreFocus?: b
626639
if (nextElement) {
627640
focusElement(nextElement, true);
628641
} else {
629-
// If there is no next element and the nodeToRestore isn't within a FocusScope (i.e. we are leaving the top level focus scope)
630-
// then move focus to the body.
631-
// Otherwise restore focus to the nodeToRestore (e.g menu within a popover -> tabbing to close the menu should move focus to menu trigger)
642+
// If there is no next element and the nodeToRestore isn't within a FocusScope (i.e. we are leaving the top level focus scope)
643+
// then move focus to the body.
644+
// Otherwise restore focus to the nodeToRestore (e.g menu within a popover -> tabbing to close the menu should move focus to menu trigger)
632645
if (!isElementInAnyScope(nodeToRestore)) {
633646
focusedElement.blur();
634647
} else {
@@ -639,12 +652,12 @@ function useRestoreFocus(scopeRef: RefObject<Element[] | null>, restoreFocus?: b
639652
};
640653

641654
if (!contain) {
642-
ownerDocument.addEventListener('keydown', onKeyDown, true);
655+
ownerDocument.addEventListener('keydown', onKeyDown as EventListener, true);
643656
}
644657

645658
return () => {
646659
if (!contain) {
647-
ownerDocument.removeEventListener('keydown', onKeyDown, true);
660+
ownerDocument.removeEventListener('keydown', onKeyDown as EventListener, true);
648661
}
649662
};
650663
}, [scopeRef, restoreFocus, contain]);
@@ -670,11 +683,12 @@ function useRestoreFocus(scopeRef: RefObject<Element[] | null>, restoreFocus?: b
670683
let nodeToRestore = treeNode.nodeToRestore;
671684

672685
// if we already lost focus to the body and this was the active scope, then we should attempt to restore
686+
let activeElement = getActiveElement(ownerDocument);
673687
if (
674688
restoreFocus
675689
&& nodeToRestore
676690
&& (
677-
((ownerDocument.activeElement && isElementInChildScope(ownerDocument.activeElement, scopeRef)) || (ownerDocument.activeElement === ownerDocument.body && shouldRestoreFocus(scopeRef)))
691+
((activeElement && isElementInChildScope(activeElement, scopeRef)) || (activeElement === ownerDocument.body && shouldRestoreFocus(scopeRef)))
678692
)
679693
) {
680694
// freeze the focusScopeTree so it persists after the raf, otherwise during unmount nodes are removed from it
@@ -723,10 +737,19 @@ function restoreFocusToElement(node: FocusableElement) {
723737
* Create a [TreeWalker]{@link https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker}
724738
* that matches all focusable/tabbable elements.
725739
*/
726-
export function getFocusableTreeWalker(root: Element, opts?: FocusManagerOptions, scope?: Element[]) {
740+
export function getFocusableTreeWalker(root: Element, opts?: FocusManagerOptions, scope?: Element[]): ShadowTreeWalker | TreeWalker {
727741
let filter = opts?.tabbable ? isTabbable : isFocusable;
728-
let walker = getOwnerDocument(root).createTreeWalker(
729-
root,
742+
743+
// Ensure that root is an Element or fall back appropriately
744+
let rootElement = root?.nodeType === Node.ELEMENT_NODE ? (root as Element) : null;
745+
746+
// Determine the document to use
747+
let doc = getOwnerDocument(rootElement);
748+
749+
// Create a TreeWalker, ensuring the root is an Element or Document
750+
let walker = createShadowTreeWalker(
751+
doc,
752+
root || doc,
730753
NodeFilter.SHOW_ELEMENT,
731754
{
732755
acceptNode(node) {
@@ -766,7 +789,7 @@ export function createFocusManager(ref: RefObject<Element | null>, defaultOption
766789
return null;
767790
}
768791
let {from, tabbable = defaultOptions.tabbable, wrap = defaultOptions.wrap, accept = defaultOptions.accept} = opts;
769-
let node = from || getOwnerDocument(root).activeElement;
792+
let node = from || getActiveElement(getOwnerDocument(root));
770793
let walker = getFocusableTreeWalker(root, {tabbable, accept});
771794
if (root.contains(node)) {
772795
walker.currentNode = node!;
@@ -787,7 +810,7 @@ export function createFocusManager(ref: RefObject<Element | null>, defaultOption
787810
return null;
788811
}
789812
let {from, tabbable = defaultOptions.tabbable, wrap = defaultOptions.wrap, accept = defaultOptions.accept} = opts;
790-
let node = from || getOwnerDocument(root).activeElement;
813+
let node = from || getActiveElement(getOwnerDocument(root));
791814
let walker = getFocusableTreeWalker(root, {tabbable, accept});
792815
if (root.contains(node)) {
793816
walker.currentNode = node!;
@@ -842,7 +865,7 @@ export function createFocusManager(ref: RefObject<Element | null>, defaultOption
842865
};
843866
}
844867

845-
function last(walker: TreeWalker) {
868+
function last(walker: ShadowTreeWalker | TreeWalker) {
846869
let next: FocusableElement | undefined = undefined;
847870
let last: FocusableElement;
848871
do {

packages/@react-aria/focus/src/focusSafely.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
*/
1212

1313
import {FocusableElement} from '@react-types/shared';
14-
import {focusWithoutScrolling, getOwnerDocument, runAfterTransition} from '@react-aria/utils';
14+
import {
15+
focusWithoutScrolling,
16+
getActiveElement,
17+
getOwnerDocument,
18+
runAfterTransition
19+
} from '@react-aria/utils';
1520
import {getInteractionModality} from '@react-aria/interactions';
1621

1722
/**
@@ -25,11 +30,12 @@ export function focusSafely(element: FocusableElement) {
2530
// causing the page to scroll when moving focus if the element is transitioning
2631
// from off the screen.
2732
const ownerDocument = getOwnerDocument(element);
33+
const activeElement = getActiveElement(ownerDocument);
2834
if (getInteractionModality() === 'virtual') {
29-
let lastFocusedElement = ownerDocument.activeElement;
35+
let lastFocusedElement = activeElement;
3036
runAfterTransition(() => {
3137
// If focus did not move and the element is still in the document, focus it.
32-
if (ownerDocument.activeElement === lastFocusedElement && element.isConnected) {
38+
if (getActiveElement(ownerDocument) === lastFocusedElement && element.isConnected) {
3339
focusWithoutScrolling(element);
3440
}
3541
});

0 commit comments

Comments
 (0)