diff --git a/packages/react/package.json b/packages/react/package.json
index d4ae30e6638..21c1ee30aa9 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..a6746108422 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,50 @@ 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.',
+ },
+ },
+ },
+}
diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css b/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css
index 5edbdb0a043..f9b98ee3d5e 100644
--- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css
+++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css
@@ -19,6 +19,7 @@
flip-inline,
flip-block flip-inline;
position-visibility: anchors-visible;
+ position-try-order: most-inline-size;
z-index: 100;
position: fixed !important;
@@ -35,23 +36,37 @@
/* stylelint-disable primer/spacing */
top: calc(anchor(bottom) + var(--base-size-4));
left: anchor(left);
+ max-height: calc(100dvh - anchor(bottom) - var(--base-size-4));
+
+ &[data-align='left'] {
+ left: auto;
+ right: calc(anchor(right) - var(--anchored-overlay-anchor-offset-left));
+ }
}
&[data-side='outside-top'] {
margin-bottom: var(--base-size-4);
bottom: anchor(top);
left: anchor(left);
+ max-height: calc(anchor(top) - var(--base-size-4));
+
+ &[data-align='left'] {
+ left: auto;
+ right: anchor(right);
+ }
}
&[data-side='outside-left'] {
right: anchor(left);
top: anchor(top);
margin-right: var(--base-size-4);
+ max-height: calc(100dvh - anchor(top));
}
&[data-side='outside-right'] {
left: anchor(right);
top: anchor(top);
margin-left: var(--base-size-4);
+ max-height: calc(100dvh - anchor(top));
}
}
diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
index 65af71b83dd..48fda14a9a6 100644
--- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
+++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
@@ -16,6 +16,7 @@ import {XIcon} from '@primer/octicons-react'
import classes from './AnchoredOverlay.module.css'
import {clsx} from 'clsx'
import {useFeatureFlag} from '../FeatureFlags'
+import {widthMap} from '../Overlay/Overlay'
interface AnchoredOverlayPropsWithAnchor {
/**
@@ -125,17 +126,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 +163,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 +224,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,
@@ -282,6 +269,18 @@ export const AnchoredOverlay: React.FC= overlayWidth) return null
+
+ const horizontal = vw - rect.right >= rect.left ? 'right' : 'left'
+ const offset = overlayWidth - rect.left
+
+ return {horizontal, offset}
+}
+
function assignRef(
ref: React.MutableRefObject | ((instance: T | null) => void) | null | undefined,
value: T | null,
diff --git a/packages/react/src/Overlay/Overlay.tsx b/packages/react/src/Overlay/Overlay.tsx
index f495d015730..9dcd41da021 100644
--- a/packages/react/src/Overlay/Overlay.tsx
+++ b/packages/react/src/Overlay/Overlay.tsx
@@ -33,8 +33,7 @@ export const heightMap = {
'fit-content': 'fit-content',
}
-// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-useless-assignment
-const widthMap = {
+export const widthMap = {
small: '256px',
medium: '320px',
large: '480px',