diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index 65af71b83dd..28e03817466 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -125,10 +125,20 @@ export type AnchoredOverlayProps = AnchoredOverlayBaseProps & (AnchoredOverlayPropsWithAnchor | AnchoredOverlayPropsWithoutAnchor) & Partial> -const applyAnchorPositioningPolyfill = async () => { - if (typeof window !== 'undefined' && !('anchorName' in document.documentElement.style)) { +// Check if native CSS anchor positioning is supported +const supportsNativeAnchorPositioning = () => + typeof window !== 'undefined' && 'anchorName' in document.documentElement.style + +const applyAnchorPositioningPolyfill = async (element: HTMLElement) => { + if (!supportsNativeAnchorPositioning()) { try { - await import('@oddbird/css-anchor-positioning') + const {default: polyfill} = await import('@oddbird/css-anchor-positioning/fn') + + polyfill({ + elements: [element], + excludeInlineStyles: false, + useAnimationFrame: true, + }) } catch (e) { // eslint-disable-next-line no-console console.warn('Failed to load CSS anchor positioning polyfill:', e) @@ -136,6 +146,35 @@ const applyAnchorPositioningPolyfill = async () => { } } +// Helper to set CSS anchor properties in a way that works with the polyfill. +// When native support exists, use setProperty (cleaner). +// TODO: Remove setAttribute path when we drop polyfill support. +function setAnchorStyle(el: HTMLElement, property: 'anchor-name' | 'position-anchor', value: string) { + if (supportsNativeAnchorPositioning()) { + el.style.setProperty(property, value) + } else { + // Polyfill path: use setAttribute to bypass browser CSS parsing + const existingStyle = el.getAttribute('style') || '' + if (!existingStyle.includes(`${property}:`)) { + // Trim trailing semicolons/whitespace to avoid double semicolons + const trimmedStyle = existingStyle.replace(/;\s*$/, '') + const newStyle = trimmedStyle ? `${trimmedStyle}; ${property}: ${value}` : `${property}: ${value}` + el.setAttribute('style', newStyle) + } + } +} + +function removeAnchorStyle(el: HTMLElement, property: 'anchor-name' | 'position-anchor') { + if (supportsNativeAnchorPositioning()) { + el.style.removeProperty(property) + } else { + // Polyfill path: remove from style attribute via regex + const existingStyle = el.getAttribute('style') || '' + const newStyle = existingStyle.replace(new RegExp(`\\s*;?\\s*${property}:[^;]*;?`, 'gi'), '') + el.setAttribute('style', newStyle.replace(/^;\s*/, '').replace(/;\s*$/, '')) + } +} + const defaultVariant = { regular: 'anchored', narrow: 'anchored', @@ -232,19 +271,12 @@ export const AnchoredOverlay: React.FC { // 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, @@ -261,12 +293,12 @@ export const AnchoredOverlay: React.FC { - anchor.style.removeProperty('anchor-name') + removeAnchorStyle(anchor, 'anchor-name') if (overlay) { - overlay.style.removeProperty('position-anchor') + removeAnchorStyle(overlay, 'position-anchor') } } }, [cssAnchorPositioning, anchorRef, overlayRef, id, open]) @@ -281,7 +313,11 @@ export const AnchoredOverlay: React.FC