diff --git a/e2e/components/AnchoredOverlay.test.ts b/e2e/components/AnchoredOverlay.test.ts index 37bf305df1a..2225eb35335 100644 --- a/e2e/components/AnchoredOverlay.test.ts +++ b/e2e/components/AnchoredOverlay.test.ts @@ -7,6 +7,7 @@ const stories: Array<{ title: string id: string viewport?: keyof typeof viewports + customViewport?: {width: number; height: number} waitForText?: string buttonName?: string buttonNames?: string[] @@ -119,6 +120,26 @@ const stories: Array<{ id: 'components-anchoredoverlay-dev--reposition-after-content-grows-within-dialog', waitForText: 'content with 300px height', }, + { + title: 'Small Viewport Right Aligned', + id: 'components-anchoredoverlay-dev--small-viewport-right-aligned', + customViewport: {width: 455, height: 858}, + }, + { + title: 'Small Viewport Outside Top', + id: 'components-anchoredoverlay-dev--small-viewport-outside-top', + customViewport: {width: 455, height: 858}, + }, + { + title: 'Small Viewport Outside Right', + id: 'components-anchoredoverlay-dev--small-viewport-outside-right', + customViewport: {width: 455, height: 858}, + }, + { + title: 'Small Viewport Outside Left', + id: 'components-anchoredoverlay-dev--small-viewport-outside-left', + customViewport: {width: 455, height: 858}, + }, ] as const const theme = 'light' @@ -142,7 +163,9 @@ test.describe('AnchoredOverlay', () => { }, }) - if (story.viewport) { + if (story.customViewport) { + await page.setViewportSize(story.customViewport) + } else if (story.viewport) { await page.setViewportSize({ width: viewports[story.viewport], height: 667, diff --git a/packages/react/package.json b/packages/react/package.json index eca897e2ae3..dc8b3f275db 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..d645fe35b7f 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,194 @@ export const WithActionMenu = { }, }, } + +export const SmallViewportRightAligned = { + render: () => { + const [open, setOpen] = useState(false) + + return ( +
+ setOpen(true)} + onClose={() => setOpen(false)} + renderAnchor={props => } + 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.', + }, + }, + }, +} + +export const SmallViewportOutsideTop = { + render: () => { + const [open, setOpen] = useState(false) + + return ( +
+ setOpen(true)} + onClose={() => setOpen(false)} + renderAnchor={props => } + side="outside-top" + overlayProps={{ + role: 'dialog', + 'aria-modal': true, + 'aria-label': 'Small viewport outside-top positioning test', + style: {minWidth: '320px'}, + }} + width="xlarge" + focusZoneSettings={{disabled: true}} + preventOverflow={false} + > +
+ + Overlay content (outside-top) + + This overlay opens above the anchor on a small viewport. It should reposition to avoid overflowing the + viewport. + + +
+
+
+ ) + }, + parameters: { + viewport: { + defaultViewport: 'small', + }, + docs: { + description: { + story: + 'Tests overlay positioning with side="outside-top" when the trigger button is right-aligned at the bottom of a small viewport.', + }, + }, + }, +} + +export const SmallViewportOutsideRight = { + render: () => { + const [open, setOpen] = useState(false) + + return ( +
+ setOpen(true)} + onClose={() => setOpen(false)} + renderAnchor={props => } + side="outside-right" + overlayProps={{ + role: 'dialog', + 'aria-modal': true, + 'aria-label': 'Small viewport outside-right positioning test', + style: {minWidth: '320px'}, + }} + width="xlarge" + focusZoneSettings={{disabled: true}} + preventOverflow={false} + > +
+ + Overlay content (outside-right) + + This overlay opens to the right of the anchor on a small viewport. It should reposition to avoid + overflowing the viewport. + + +
+
+
+ ) + }, + parameters: { + viewport: { + defaultViewport: 'small', + }, + docs: { + description: { + story: + 'Tests overlay positioning with side="outside-right" when the trigger button is left-aligned on a small viewport.', + }, + }, + }, +} + +export const SmallViewportOutsideLeft = { + render: () => { + const [open, setOpen] = useState(false) + + return ( +
+ setOpen(true)} + onClose={() => setOpen(false)} + renderAnchor={props => } + side="outside-left" + overlayProps={{ + role: 'dialog', + 'aria-modal': true, + 'aria-label': 'Small viewport outside-left positioning test', + style: {minWidth: '320px'}, + }} + width="xlarge" + focusZoneSettings={{disabled: true}} + preventOverflow={false} + > +
+ + Overlay content (outside-left) + + This overlay opens to the left of the anchor on a small viewport. It should reposition to avoid + overflowing the viewport. + + +
+
+
+ ) + }, + parameters: { + viewport: { + defaultViewport: 'small', + }, + docs: { + description: { + story: + 'Tests overlay positioning with side="outside-left" when the trigger button is right-aligned on a small viewport.', + }, + }, + }, +} diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index 65af71b83dd..30502f672cd 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -125,17 +125,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 +162,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 +223,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,