From a4f4880e2bf68bdba273ecc3f7120105cc8edbc1 Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Wed, 15 Apr 2026 22:39:21 -0400 Subject: [PATCH 1/2] Add prop to hide overlay if anchor is not visible in the viewport --- .../src/AnchoredOverlay/AnchoredOverlay.tsx | 18 +++++- packages/react/src/hooks/index.ts | 1 + .../react/src/hooks/useAnchorVisibility.ts | 62 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 packages/react/src/hooks/useAnchorVisibility.ts diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index 7421e6a84d6..7b27b8f8244 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -7,7 +7,7 @@ import type {FocusTrapHookSettings} from '../hooks/useFocusTrap' import {useFocusTrap} from '../hooks/useFocusTrap' import type {FocusZoneHookSettings} from '../hooks/useFocusZone' import {useFocusZone} from '../hooks/useFocusZone' -import {useAnchoredPosition, useProvidedRefOrCreate, useRenderForcingRef} from '../hooks' +import {useAnchoredPosition, useProvidedRefOrCreate, useRenderForcingRef, useAnchorVisibility} from '../hooks' import {useId} from '../hooks/useId' import type {AnchorPosition, PositionSettings} from '@primer/behaviors' import {type ResponsiveValue} from '../hooks/useResponsiveValue' @@ -120,6 +120,13 @@ interface AnchoredOverlayBaseProps extends Pick + /** + * When enabled (and CSS anchor positioning feature flag is on), hides the overlay + * when the anchor element scrolls out of the viewport. This uses IntersectionObserver + * to track anchor visibility. + * @default false + */ + hideOnAnchorHidden?: boolean } export type AnchoredOverlayProps = AnchoredOverlayBaseProps & @@ -162,6 +169,7 @@ export const AnchoredOverlay: React.FC { const cssAnchorPositioningFlag = useFeatureFlag('primer_react_css_anchor_positioning') const supportsNativeCSSAnchorPositioning = useRef(false) @@ -170,6 +178,12 @@ export const AnchoredOverlay: React.FC() const anchorId = useId(externalAnchorId) + // Track anchor visibility to hide overlay when anchor scrolls out of viewport. + // This provides a JS fallback for CSS `position-visibility: anchors-visible` + // which only considers overflow clipping, not viewport visibility. + // Only enabled when both CSS anchor positioning is active AND hideOnAnchorHidden prop is true. + const isAnchorVisible = useAnchorVisibility(anchorRef, cssAnchorPositioning && open && hideOnAnchorHidden) + const onClickOutside = useCallback(() => onClose?.('click-outside'), [onClose]) const onEscape = useCallback(() => onClose?.('escape'), [onClose]) @@ -317,7 +331,7 @@ export const AnchoredOverlay: React.FC, + enabled = true, + threshold = 0, +): boolean { + const [isAnchorVisible, setIsAnchorVisible] = useState(true) + + useEffect(() => { + // When disabled or no anchor, don't set up the observer + // The hook returns true by default, so the overlay stays visible + if (!enabled || !anchorRef.current) { + return + } + + const anchor = anchorRef.current + + const observer = new IntersectionObserver( + entries => { + for (const entry of entries) { + // isIntersecting is true when any part of the element is visible + // (based on threshold). When threshold is 0, this means any pixel. + setIsAnchorVisible(entry.isIntersecting) + } + }, + { + // Use null for root to observe against the viewport + root: null, + // No margin adjustments needed + rootMargin: '0px', + // threshold of 0 means callback fires as soon as even one pixel is visible/hidden + threshold, + }, + ) + + observer.observe(anchor) + + return () => { + observer.disconnect() + } + }, [anchorRef, enabled, threshold]) + + // When disabled, always return true so overlay remains visible + if (!enabled) { + return true + } + + return isAnchorVisible +} From f80688b3f343fac58a769a0e1298b609bcd93738 Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Wed, 15 Apr 2026 23:32:31 -0400 Subject: [PATCH 2/2] Make `true` by default --- packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index 7b27b8f8244..41122678791 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -169,7 +169,7 @@ export const AnchoredOverlay: React.FC { const cssAnchorPositioningFlag = useFeatureFlag('primer_react_css_anchor_positioning') const supportsNativeCSSAnchorPositioning = useRef(false)