From 23abe5bd42168b1a7319e5a83afc1c0b87874751 Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Wed, 27 May 2026 13:23:45 -0400 Subject: [PATCH 1/2] Allow for customizing CSS anchor positioning --- .../AnchoredOverlay.dev.stories.tsx | 100 ++++++++++++++++++ .../AnchoredOverlay.features.stories.tsx | 1 + .../AnchoredOverlay/AnchoredOverlay.test.tsx | 66 ++++++++++++ .../src/AnchoredOverlay/AnchoredOverlay.tsx | 59 ++++++++++- 4 files changed, 225 insertions(+), 1 deletion(-) diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx index fc95111e9a3..dc9cb7331a6 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx @@ -5,6 +5,11 @@ import {Button} from '../Button' import {AnchoredOverlay} from '.' import {Stack} from '../Stack' import {Dialog, Spinner, ActionList, ActionMenu} from '..' +import Octicon from '../Octicon' +import Avatar from '../Avatar' +import Link from '../Link' +import {LocationIcon, RepoIcon} from '@primer/octicons-react' +import classes from './AnchoredOverlay.features.stories.module.css' const meta = { title: 'Components/AnchoredOverlay/Dev', @@ -13,6 +18,34 @@ const meta = { export default meta +const hoverCard = ( + + + + + + + monalisa + + + Monalisa Octocat + + + + + Former beach cat and champion swimmer. Now your friendly octopus with a normal face. + + + + Interwebs + + + + Owns this repository + + +) + export const RepositionAfterContentGrows = () => { const [open, setOpen] = useState(false) const [loading, setLoading] = useState(true) @@ -345,3 +378,70 @@ export const ManyOverlays = () => { ) } + +const gridPositions = [ + {row: 'start', col: 'start'}, + {row: 'start', col: 'center'}, + {row: 'start', col: 'end'}, + {row: 'center', col: 'start'}, + {row: 'center', col: 'center'}, + {row: 'center', col: 'end'}, + {row: 'end', col: 'start'}, + {row: 'end', col: 'center'}, + {row: 'end', col: 'end'}, +] + +export const AnchorPositionGridFallbackDisabled = () => { + return +} + +const AnchorPositionGridFallback = ({ + cssAnchorPositioningSettings, +}: { + cssAnchorPositioningSettings: {fallbackStrategy: 'none' | 'opposite-side'} +}) => { + const [openCell, setOpenCell] = useState(null) + + return ( +
+
+
+ {gridPositions.map(({row, col}) => { + const key = `${row}-${col}` + const isCenter = row === 'center' && col === 'center' + + return ( +
+ + {row} / {col} + + {isCenter ? ( + setOpenCell(key)} + onClose={() => setOpenCell(null)} + renderAnchor={props => } + overlayProps={{ + role: 'dialog', + 'aria-modal': true, + 'aria-label': 'Anchor Position Grid Demo', + }} + focusZoneSettings={{disabled: true}} + preventOverflow={false} + cssAnchorPositioningSettings={cssAnchorPositioningSettings} + > +
{hoverCard}
+
+ ) : null} +
+ ) + })} +
+
+
+ ) +} + +export const AnchorPositionGridFallbackOppositeSide = () => { + return +} diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx index 9656519d5a8..c5c41a22090 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx @@ -402,6 +402,7 @@ export const CenteredOnPage = () => { 'aria-modal': true, 'aria-label': 'Centered Overlay Demo', }} + cssAnchorPositioningSettings={{fallbackStrategy: 'default'}} focusZoneSettings={{disabled: true}} preventOverflow={false} > diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx index 255c771c6b3..1082caf95de 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx @@ -354,6 +354,50 @@ describe('AnchoredOverlay feature flag specific behavior', () => { expect(overlay).not.toHaveAttribute('popover') }) + it('should disable CSS side fallbacks when cssAnchorPositioningSettings.fallbackStrategy is "none"', () => { + const {baseElement} = render( + + + {}} + onClose={() => {}} + renderAnchor={props => } + side="outside-bottom" + cssAnchorPositioningSettings={{fallbackStrategy: 'none'}} + > + + + + , + ) + + const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') as HTMLElement + expect(overlay.style.getPropertyValue('position-try-fallbacks')).toBe('none') + }) + + it('should only allow opposite-side CSS fallback when cssAnchorPositioningSettings.fallbackStrategy is "opposite-side"', () => { + const {baseElement} = render( + + + {}} + onClose={() => {}} + renderAnchor={props => } + side="outside-bottom" + cssAnchorPositioningSettings={{fallbackStrategy: 'opposite-side'}} + > + + + + , + ) + + const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') as HTMLElement + expect(overlay.style.getPropertyValue('position-try-fallbacks')).toBe('flip-block') + }) + describe('when overlayProps.portalContainerName is provided', () => { it('should fall back to JS positioning (data-anchor-position="false") even with the flag enabled', () => { const portalRoot = document.createElement('div') @@ -451,6 +495,28 @@ describe('AnchoredOverlay feature flag specific behavior', () => { const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') expect(overlay).not.toHaveAttribute('popover') }) + + it('should ignore cssAnchorPositioningSettings when CSS anchor positioning is disabled', () => { + const {baseElement} = render( + + + {}} + onClose={() => {}} + renderAnchor={props => } + side="outside-bottom" + cssAnchorPositioningSettings={{fallbackStrategy: 'none'}} + > + + + + , + ) + + const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') as HTMLElement + expect(overlay.style.getPropertyValue('position-try-fallbacks')).toBe('') + }) }) }) diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index e0050775587..c08fa723a8a 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -124,6 +124,18 @@ interface AnchoredOverlayBaseProps extends Pick { const cssAnchorPositioningFlag = useFeatureFlag('primer_react_css_anchor_positioning') // Lazy initial state so feature detection runs once per mount on the client. @@ -294,6 +307,17 @@ export const AnchoredOverlay: React.FC { @@ -337,11 +361,22 @@ export const AnchoredOverlay: React.FC( ref: React.MutableRefObject | ((instance: T | null) => void) | null | undefined, value: T | null, From c0bde01a4df4fa505e1fab5dd5dc6616146abf09 Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Fri, 29 May 2026 15:08:04 -0400 Subject: [PATCH 2/2] Ensure strategy is not set unless given --- .../src/AnchoredOverlay/AnchoredOverlay.tsx | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index 16453b2080c..4e278da02d0 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -513,22 +513,26 @@ function getCSSAnchorPositionTryFallbacks( side: PositionSettings['side'], strategy: 'default' | 'none' | 'opposite-side', ): string | undefined { - // We bail out here, and use the styles in styles. + // Disable all CSS fallbacks (including those defined in the stylesheet). if (strategy === 'none') return 'none' - switch (side) { - case 'outside-top': - case 'outside-bottom': { - return 'flip-block' - } - case 'outside-left': - case 'outside-right': { - return 'flip-inline' - } - default: { - return undefined + // Restrict fallbacks to flipping to the opposite side along the relevant axis. + if (strategy === 'opposite-side') { + switch (side) { + case 'outside-top': + case 'outside-bottom': + return 'flip-block' + case 'outside-left': + case 'outside-right': + return 'flip-inline' + default: + return undefined } } + + // 'default': don't write an inline `position-try-fallbacks`, so the + // stylesheet-defined fallback list on `.AnchoredOverlay` takes effect. + return undefined } function assignRef(