diff --git a/e2e/components/AnchoredOverlay.test.ts b/e2e/components/AnchoredOverlay.test.ts
index 37bf305df1a..2225eb35335 100644
--- a/e2e/components/AnchoredOverlay.test.ts
+++ b/e2e/components/AnchoredOverlay.test.ts
@@ -7,6 +7,7 @@ const stories: Array<{
title: string
id: string
viewport?: keyof typeof viewports
+ customViewport?: {width: number; height: number}
waitForText?: string
buttonName?: string
buttonNames?: string[]
@@ -119,6 +120,26 @@ const stories: Array<{
id: 'components-anchoredoverlay-dev--reposition-after-content-grows-within-dialog',
waitForText: 'content with 300px height',
},
+ {
+ title: 'Small Viewport Right Aligned',
+ id: 'components-anchoredoverlay-dev--small-viewport-right-aligned',
+ customViewport: {width: 455, height: 858},
+ },
+ {
+ title: 'Small Viewport Outside Top',
+ id: 'components-anchoredoverlay-dev--small-viewport-outside-top',
+ customViewport: {width: 455, height: 858},
+ },
+ {
+ title: 'Small Viewport Outside Right',
+ id: 'components-anchoredoverlay-dev--small-viewport-outside-right',
+ customViewport: {width: 455, height: 858},
+ },
+ {
+ title: 'Small Viewport Outside Left',
+ id: 'components-anchoredoverlay-dev--small-viewport-outside-left',
+ customViewport: {width: 455, height: 858},
+ },
] as const
const theme = 'light'
@@ -142,7 +163,9 @@ test.describe('AnchoredOverlay', () => {
},
})
- if (story.viewport) {
+ if (story.customViewport) {
+ await page.setViewportSize(story.customViewport)
+ } else if (story.viewport) {
await page.setViewportSize({
width: viewports[story.viewport],
height: 667,
diff --git a/packages/react/package.json b/packages/react/package.json
index eca897e2ae3..dc8b3f275db 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -78,7 +78,6 @@
"@github/relative-time-element": "^4.5.0",
"@github/tab-container-element": "^4.8.2",
"@lit-labs/react": "1.2.1",
- "@oddbird/css-anchor-positioning": "^0.9.0",
"@oddbird/popover-polyfill": "^0.5.2",
"@primer/behaviors": "^1.10.2",
"@primer/live-region-element": "^0.7.1",
diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx
index 88f7a778309..d645fe35b7f 100644
--- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx
+++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx
@@ -4,7 +4,7 @@ import React, {useState, useRef} from 'react'
import {Button} from '../Button'
import {AnchoredOverlay} from '.'
import {Stack} from '../Stack'
-import {Dialog, Spinner, ActionList, ActionMenu} from '..'
+import {Dialog, Spinner, ActionList, ActionMenu, Text} from '..'
const meta = {
title: 'Components/AnchoredOverlay/Dev',
@@ -309,3 +309,194 @@ export const WithActionMenu = {
},
},
}
+
+export const SmallViewportRightAligned = {
+ render: () => {
+ const [open, setOpen] = useState(false)
+
+ return (
+
+
setOpen(true)}
+ onClose={() => setOpen(false)}
+ renderAnchor={props => Button }
+ overlayProps={{
+ role: 'dialog',
+ 'aria-modal': true,
+ 'aria-label': 'Small viewport positioning test',
+ style: {minWidth: '320px'},
+ }}
+ width="xlarge"
+ focusZoneSettings={{disabled: true}}
+ preventOverflow={false}
+ >
+
+
+ Overlay content
+
+ This overlay is wider than the available space to the left of the anchor. It should reposition to avoid
+ overflowing the viewport.
+
+
+
+
+
+ )
+ },
+ parameters: {
+ viewport: {
+ defaultViewport: 'small',
+ },
+ docs: {
+ description: {
+ story:
+ 'Tests overlay positioning when the trigger button is right-aligned on a small viewport. The overlay is wider than the space to the left of the anchor.',
+ },
+ },
+ },
+}
+
+export const SmallViewportOutsideTop = {
+ render: () => {
+ const [open, setOpen] = useState(false)
+
+ return (
+
+
setOpen(true)}
+ onClose={() => setOpen(false)}
+ renderAnchor={props => Button }
+ side="outside-top"
+ overlayProps={{
+ role: 'dialog',
+ 'aria-modal': true,
+ 'aria-label': 'Small viewport outside-top positioning test',
+ style: {minWidth: '320px'},
+ }}
+ width="xlarge"
+ focusZoneSettings={{disabled: true}}
+ preventOverflow={false}
+ >
+
+
+ Overlay content (outside-top)
+
+ This overlay opens above the anchor on a small viewport. It should reposition to avoid overflowing the
+ viewport.
+
+
+
+
+
+ )
+ },
+ parameters: {
+ viewport: {
+ defaultViewport: 'small',
+ },
+ docs: {
+ description: {
+ story:
+ 'Tests overlay positioning with side="outside-top" when the trigger button is right-aligned at the bottom of a small viewport.',
+ },
+ },
+ },
+}
+
+export const SmallViewportOutsideRight = {
+ render: () => {
+ const [open, setOpen] = useState(false)
+
+ return (
+
+
setOpen(true)}
+ onClose={() => setOpen(false)}
+ renderAnchor={props => Button }
+ side="outside-right"
+ overlayProps={{
+ role: 'dialog',
+ 'aria-modal': true,
+ 'aria-label': 'Small viewport outside-right positioning test',
+ style: {minWidth: '320px'},
+ }}
+ width="xlarge"
+ focusZoneSettings={{disabled: true}}
+ preventOverflow={false}
+ >
+
+
+ Overlay content (outside-right)
+
+ This overlay opens to the right of the anchor on a small viewport. It should reposition to avoid
+ overflowing the viewport.
+
+
+
+
+
+ )
+ },
+ parameters: {
+ viewport: {
+ defaultViewport: 'small',
+ },
+ docs: {
+ description: {
+ story:
+ 'Tests overlay positioning with side="outside-right" when the trigger button is left-aligned on a small viewport.',
+ },
+ },
+ },
+}
+
+export const SmallViewportOutsideLeft = {
+ render: () => {
+ const [open, setOpen] = useState(false)
+
+ return (
+
+
setOpen(true)}
+ onClose={() => setOpen(false)}
+ renderAnchor={props => Button }
+ side="outside-left"
+ overlayProps={{
+ role: 'dialog',
+ 'aria-modal': true,
+ 'aria-label': 'Small viewport outside-left positioning test',
+ style: {minWidth: '320px'},
+ }}
+ width="xlarge"
+ focusZoneSettings={{disabled: true}}
+ preventOverflow={false}
+ >
+
+
+ Overlay content (outside-left)
+
+ This overlay opens to the left of the anchor on a small viewport. It should reposition to avoid
+ overflowing the viewport.
+
+
+
+
+
+ )
+ },
+ parameters: {
+ viewport: {
+ defaultViewport: 'small',
+ },
+ docs: {
+ description: {
+ story:
+ 'Tests overlay positioning with side="outside-left" when the trigger button is right-aligned on a small viewport.',
+ },
+ },
+ },
+}
diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
index 65af71b83dd..30502f672cd 100644
--- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
+++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
@@ -125,17 +125,6 @@ export type AnchoredOverlayProps = AnchoredOverlayBaseProps &
(AnchoredOverlayPropsWithAnchor | AnchoredOverlayPropsWithoutAnchor) &
Partial>
-const applyAnchorPositioningPolyfill = async () => {
- if (typeof window !== 'undefined' && !('anchorName' in document.documentElement.style)) {
- try {
- await import('@oddbird/css-anchor-positioning')
- } catch (e) {
- // eslint-disable-next-line no-console
- console.warn('Failed to load CSS anchor positioning polyfill:', e)
- }
- }
-}
-
const defaultVariant = {
regular: 'anchored',
narrow: 'anchored',
@@ -173,7 +162,9 @@ export const AnchoredOverlay: React.FC {
- const cssAnchorPositioning = useFeatureFlag('primer_react_css_anchor_positioning')
+ const cssAnchorPositioningFlag = useFeatureFlag('primer_react_css_anchor_positioning')
+ const supportsNativeCSSAnchorPositioning = useRef(false)
+ const cssAnchorPositioning = cssAnchorPositioningFlag && supportsNativeCSSAnchorPositioning.current
const anchorRef = useProvidedRefOrCreate(externalAnchorRef)
const [overlayRef, updateOverlayRef] = useRenderForcingRef()
const anchorId = useId(externalAnchorId)
@@ -232,19 +223,14 @@ export const AnchoredOverlay: React.FC {
+ supportsNativeCSSAnchorPositioning.current = 'anchorName' in document.documentElement.style
+
// ensure overlay ref gets cleared when closed, so position can reset between closing/re-opening
if (!open && overlayRef.current) {
updateOverlayRef(null)
}
-
- if (cssAnchorPositioning && !hasLoadedAnchorPositioningPolyfill.current) {
- applyAnchorPositioningPolyfill()
- hasLoadedAnchorPositioningPolyfill.current = true
- }
- }, [open, overlayRef, updateOverlayRef, cssAnchorPositioning])
+ }, [open, overlayRef, updateOverlayRef])
useFocusZone({
containerRef: overlayRef,