diff --git a/.changeset/eight-eagles-deny.md b/.changeset/eight-eagles-deny.md new file mode 100644 index 00000000000..986dcba3978 --- /dev/null +++ b/.changeset/eight-eagles-deny.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +AnchoredOverlay: Remove polyfill for CSS Anchor Positioning, use primer/behaviors as fallback. Ensure overlays take available space. diff --git a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Fullscreen-Variant-light-css-anchor-positioning-linux.png b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Fullscreen-Variant-light-css-anchor-positioning-linux.png index edb90f94d08..4498d424d11 100644 Binary files a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Fullscreen-Variant-light-css-anchor-positioning-linux.png and b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Fullscreen-Variant-light-css-anchor-positioning-linux.png differ diff --git a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Multiple-Overlays-External-anchor-2-light-css-anchor-positioning-linux.png b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Multiple-Overlays-External-anchor-2-light-css-anchor-positioning-linux.png index a5e7cac9e56..dcf7b73bc0d 100644 Binary files a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Multiple-Overlays-External-anchor-2-light-css-anchor-positioning-linux.png and b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Multiple-Overlays-External-anchor-2-light-css-anchor-positioning-linux.png differ diff --git a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Overlay-Props-Overrides-light-css-anchor-positioning-linux.png b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Overlay-Props-Overrides-light-css-anchor-positioning-linux.png index 12045d83e16..eda5126cb55 100644 Binary files a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Overlay-Props-Overrides-light-css-anchor-positioning-linux.png and b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Overlay-Props-Overrides-light-css-anchor-positioning-linux.png differ diff --git a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Width-light-css-anchor-positioning-linux.png b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Width-light-css-anchor-positioning-linux.png index f407bffc82d..309c69e4b9f 100644 Binary files a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Width-light-css-anchor-positioning-linux.png and b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Width-light-css-anchor-positioning-linux.png differ 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.module.css b/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css index 5edbdb0a043..f40e73c6283 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css @@ -27,7 +27,6 @@ margin: 0; padding: 0; border: 0; - max-height: none; max-width: none; } @@ -35,23 +34,49 @@ /* stylelint-disable primer/spacing */ top: calc(anchor(bottom) + var(--base-size-4)); left: anchor(left); + + &[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); + + &[data-align='left'] { + left: auto; + right: anchor(right); + } } &[data-side='outside-left'] { right: anchor(left); top: anchor(top); margin-right: var(--base-size-4); + position-try-fallbacks: flip-inline, flip-block, flip-start, --outside-left-to-bottom; } &[data-side='outside-right'] { left: anchor(right); top: anchor(top); margin-left: var(--base-size-4); + position-try-fallbacks: flip-inline, flip-block, flip-start, --outside-right-to-bottom; } } + +@position-try --outside-left-to-bottom { + right: anchor(right); + top: calc(anchor(bottom) + var(--base-size-4)); + margin: 0; + width: auto; +} + +@position-try --outside-right-to-bottom { + left: anchor(left); + top: calc(anchor(bottom) + var(--base-size-4)); + margin: 0; + width: auto; +} diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index 65af71b83dd..7421e6a84d6 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,19 @@ export const AnchoredOverlay: React.FC spaceRight ? 'left' : 'right' + + // If there's no explicit overlay width, or either side has enough space + // to contain the overlay, let CSS position-try-fallbacks handle positioning + if (!overlayWidth || spaceLeft >= overlayWidth + viewportMargin || spaceRight >= overlayWidth + viewportMargin) { + return {horizontal} + } + + // If the viewport is too narrow to fit the overlay on either side, calculate offsets to prevent overflow + // leftOffset is how much to shift the overlay to the right, rightOffset is how much to shift the overlay to the left + const leftOffset = Math.max(0, overlayWidth - rect.right + viewportMargin) + const rightOffset = Math.max(0, rect.left + overlayWidth - vw + viewportMargin) + + return {horizontal, leftOffset, rightOffset} +} + function assignRef( ref: React.MutableRefObject | ((instance: T | null) => void) | null | undefined, value: T | null, diff --git a/packages/react/src/Overlay/Overlay.module.css b/packages/react/src/Overlay/Overlay.module.css index 69dc40cd1b2..4d6d4bbeafa 100644 --- a/packages/react/src/Overlay/Overlay.module.css +++ b/packages/react/src/Overlay/Overlay.module.css @@ -201,7 +201,8 @@ visibility: hidden; } - &:where([data-responsive='fullscreen']) { + &:where([data-responsive='fullscreen']), + &[data-responsive='fullscreen'][data-anchor-position='true'] { @media screen and (--viewportRange-narrow) { position: fixed; top: 0; 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',