@@ -16,6 +16,7 @@ import {XIcon} from '@primer/octicons-react'
1616import classes from './AnchoredOverlay.module.css'
1717import { clsx } from 'clsx'
1818import { useFeatureFlag } from '../FeatureFlags'
19+ import { widthMap } from '../Overlay/Overlay'
1920
2021interface AnchoredOverlayPropsWithAnchor {
2122 /**
@@ -125,17 +126,6 @@ export type AnchoredOverlayProps = AnchoredOverlayBaseProps &
125126 ( AnchoredOverlayPropsWithAnchor | AnchoredOverlayPropsWithoutAnchor ) &
126127 Partial < Pick < PositionSettings , 'align' | 'side' | 'anchorOffset' | 'alignmentOffset' | 'displayInViewport' > >
127128
128- const applyAnchorPositioningPolyfill = async ( ) => {
129- if ( typeof window !== 'undefined' && ! ( 'anchorName' in document . documentElement . style ) ) {
130- try {
131- await import ( '@oddbird/css-anchor-positioning' )
132- } catch ( e ) {
133- // eslint-disable-next-line no-console
134- console . warn ( 'Failed to load CSS anchor positioning polyfill:' , e )
135- }
136- }
137- }
138-
139129const defaultVariant = {
140130 regular : 'anchored' ,
141131 narrow : 'anchored' ,
@@ -173,7 +163,9 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
173163 displayCloseButton = true ,
174164 closeButtonProps = defaultCloseButtonProps ,
175165} ) => {
176- const cssAnchorPositioning = useFeatureFlag ( 'primer_react_css_anchor_positioning' )
166+ const cssAnchorPositioningFlag = useFeatureFlag ( 'primer_react_css_anchor_positioning' )
167+ const supportsNativeCSSAnchorPositioning = useRef ( false )
168+ const cssAnchorPositioning = cssAnchorPositioningFlag && supportsNativeCSSAnchorPositioning . current
177169 const anchorRef = useProvidedRefOrCreate ( externalAnchorRef )
178170 const [ overlayRef , updateOverlayRef ] = useRenderForcingRef < HTMLDivElement > ( )
179171 const anchorId = useId ( externalAnchorId )
@@ -232,19 +224,14 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
232224 [ overlayRef . current ] ,
233225 )
234226
235- const hasLoadedAnchorPositioningPolyfill = useRef ( false )
236-
237227 useEffect ( ( ) => {
228+ supportsNativeCSSAnchorPositioning . current = 'anchorName' in document . documentElement . style
229+
238230 // ensure overlay ref gets cleared when closed, so position can reset between closing/re-opening
239231 if ( ! open && overlayRef . current ) {
240232 updateOverlayRef ( null )
241233 }
242-
243- if ( cssAnchorPositioning && ! hasLoadedAnchorPositioningPolyfill . current ) {
244- applyAnchorPositioningPolyfill ( )
245- hasLoadedAnchorPositioningPolyfill . current = true
246- }
247- } , [ open , overlayRef , updateOverlayRef , cssAnchorPositioning ] )
234+ } , [ open , overlayRef , updateOverlayRef ] )
248235
249236 useFocusZone ( {
250237 containerRef : overlayRef ,
@@ -282,14 +269,27 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
282269
283270 if ( ! cssAnchorPositioning || ! open || ! currentOverlay ) return
284271 currentOverlay . style . setProperty ( 'position-anchor' , `--anchored-overlay-anchor-${ id } ` )
272+
273+ const anchorElement = anchorRef . current
274+ if ( anchorElement ) {
275+ const overlayWidth = width ? parseInt ( widthMap [ width ] ) : null
276+ const result = getDefaultPosition ( anchorElement , overlayWidth )
277+
278+ currentOverlay . setAttribute ( 'data-align' , result . horizontal )
279+
280+ // Apply offset only when viewport is too narrow
281+ const offset = result . horizontal === 'left' ? result . leftOffset : result . rightOffset
282+ currentOverlay . style . setProperty ( `--anchored-overlay-anchor-offset-${ result . horizontal } ` , `${ offset || 0 } px` )
283+ }
284+
285285 try {
286286 if ( ! currentOverlay . matches ( ':popover-open' ) ) {
287287 currentOverlay . showPopover ( )
288288 }
289289 } catch {
290290 // Ignore if popover is already showing or not supported
291291 }
292- } , [ cssAnchorPositioning , open , overlayElement , id , overlayRef ] )
292+ } , [ cssAnchorPositioning , open , overlayElement , id , overlayRef , anchorRef , width ] )
293293
294294 const showXIcon = onClose && variant . narrow === 'fullscreen' && displayCloseButton
295295 const XButtonAriaLabelledBy = closeButtonProps [ 'aria-labelledby' ]
@@ -365,6 +365,31 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
365365 )
366366}
367367
368+ function getDefaultPosition (
369+ anchorElement : HTMLElement ,
370+ overlayWidth : number | null ,
371+ ) : { horizontal : 'left' | 'right' ; leftOffset ?: number ; rightOffset ?: number } {
372+ const rect = anchorElement . getBoundingClientRect ( )
373+ const vw = window . innerWidth
374+ const viewportMargin = 8
375+ const spaceLeft = rect . left
376+ const spaceRight = vw - rect . right
377+ const horizontal : 'left' | 'right' = spaceLeft > spaceRight ? 'left' : 'right'
378+
379+ // If there's no explicit overlay width, or either side has enough space
380+ // to contain the overlay, let CSS position-try-fallbacks handle positioning
381+ if ( ! overlayWidth || spaceLeft >= overlayWidth + viewportMargin || spaceRight >= overlayWidth + viewportMargin ) {
382+ return { horizontal}
383+ }
384+
385+ // If the viewport is too narrow to fit the overlay on either side, calculate offsets to prevent overflow
386+ // leftOffset is how much to shift the overlay to the right, rightOffset is how much to shift the overlay to the left
387+ const leftOffset = Math . max ( 0 , overlayWidth - rect . right + viewportMargin )
388+ const rightOffset = Math . max ( 0 , rect . left + overlayWidth - vw + viewportMargin )
389+
390+ return { horizontal, leftOffset, rightOffset}
391+ }
392+
368393function assignRef < T > (
369394 ref : React . MutableRefObject < T | null > | ( ( instance : T | null ) => void ) | null | undefined ,
370395 value : T | null ,
0 commit comments