From a5ece09c044899290b2fc7ee2fac76ca7332ee66 Mon Sep 17 00:00:00 2001 From: Xiaoyan Li Date: Wed, 15 Oct 2025 13:26:23 +0800 Subject: [PATCH] fix(RAC): Respect virtual focus in useOverlayPosition --- .../overlays/src/useOverlayPosition.ts | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/@react-aria/overlays/src/useOverlayPosition.ts b/packages/@react-aria/overlays/src/useOverlayPosition.ts index 59c61a08075..adb51b013c8 100644 --- a/packages/@react-aria/overlays/src/useOverlayPosition.ts +++ b/packages/@react-aria/overlays/src/useOverlayPosition.ts @@ -17,6 +17,7 @@ import {useCallback, useEffect, useRef, useState} from 'react'; import {useCloseOnScroll} from './useCloseOnScroll'; import {useLayoutEffect, useResizeObserver} from '@react-aria/utils'; import {useLocale} from '@react-aria/i18n'; +import {getVirtuallyFocusedElement} from "@react-aria/focus"; export interface AriaPositionProps extends PositionProps { /** @@ -153,19 +154,29 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria { // This stores the offset of the anchor element from the scroll container // so it can be restored after repositioning. This way if the overlay height // changes, the focused element appears to stay in the same position. + let activeElement: Element | null = null; let anchor: ScrollAnchor | null = null; - if (scrollRef.current && scrollRef.current.contains(document.activeElement)) { - let anchorRect = document.activeElement?.getBoundingClientRect(); - let scrollRect = scrollRef.current.getBoundingClientRect(); - // Anchor from the top if the offset is in the top half of the scrollable element, - // otherwise anchor from the bottom. - anchor = { - type: 'top', - offset: (anchorRect?.top ?? 0) - scrollRect.top - }; - if (anchor.offset > scrollRect.height / 2) { - anchor.type = 'bottom'; - anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom; + if (scrollRef.current) { + if (scrollRef.current.contains(document.activeElement)) { + activeElement = document.activeElement; + } + let virtuallyFocusedElement = getVirtuallyFocusedElement(document); + if (scrollRef.current.contains(virtuallyFocusedElement)) { + activeElement = virtuallyFocusedElement; + } + if (activeElement) { + let anchorRect = activeElement.getBoundingClientRect(); + let scrollRect = scrollRef.current.getBoundingClientRect(); + // Anchor from the top if the offset is in the top half of the scrollable element, + // otherwise anchor from the bottom. + anchor = { + type: 'top', + offset: (anchorRect?.top ?? 0) - scrollRect.top + }; + if (anchor.offset > scrollRect.height / 2) { + anchor.type = 'bottom'; + anchor.offset = (anchorRect?.bottom ?? 0) - scrollRect.bottom; + } } } @@ -208,8 +219,8 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria { overlay.style.maxHeight = position.maxHeight != null ? position.maxHeight + 'px' : ''; // Restore scroll position relative to anchor element. - if (anchor && document.activeElement && scrollRef.current) { - let anchorRect = document.activeElement.getBoundingClientRect(); + if (anchor && activeElement && scrollRef.current) { + let anchorRect = activeElement.getBoundingClientRect(); let scrollRect = scrollRef.current.getBoundingClientRect(); let newOffset = anchorRect[anchor.type] - scrollRect[anchor.type]; scrollRef.current.scrollTop += newOffset - anchor.offset;