Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion e2e/components/AnchoredOverlay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -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'
Expand All @@ -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,
Expand Down
1 change: 0 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
193 changes: 192 additions & 1 deletion packages/react/src/AnchoredOverlay/AnchoredOverlay.dev.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -309,3 +309,194 @@ export const WithActionMenu = {
},
},
}

export const SmallViewportRightAligned = {
render: () => {
const [open, setOpen] = useState(false)

return (
<div style={{display: 'flex', justifyContent: 'flex-end'}}>
<AnchoredOverlay
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
overlayProps={{
role: 'dialog',
'aria-modal': true,
'aria-label': 'Small viewport positioning test',
style: {minWidth: '320px'},
}}
width="xlarge"
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<div style={{padding: '16px', width: '100%', height: '400px'}}>
<Stack gap="condensed">
<Text weight="medium">Overlay content</Text>
<Text>
This overlay is wider than the available space to the left of the anchor. It should reposition to avoid
overflowing the viewport.
</Text>
</Stack>
</div>
</AnchoredOverlay>
</div>
)
},
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 (
<div style={{display: 'flex', justifyContent: 'flex-end', alignItems: 'flex-end', height: '100vh'}}>
<AnchoredOverlay
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
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}
>
<div style={{padding: '16px', width: '100%', height: '400px'}}>
<Stack gap="condensed">
<Text weight="medium">Overlay content (outside-top)</Text>
<Text>
This overlay opens above the anchor on a small viewport. It should reposition to avoid overflowing the
viewport.
</Text>
</Stack>
</div>
</AnchoredOverlay>
</div>
)
},
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 (
<div style={{display: 'flex', justifyContent: 'flex-start'}}>
<AnchoredOverlay
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
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}
>
<div style={{padding: '16px', width: '100%', height: '400px'}}>
<Stack gap="condensed">
<Text weight="medium">Overlay content (outside-right)</Text>
<Text>
This overlay opens to the right of the anchor on a small viewport. It should reposition to avoid
overflowing the viewport.
</Text>
</Stack>
</div>
</AnchoredOverlay>
</div>
)
},
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 (
<div style={{display: 'flex', justifyContent: 'flex-end'}}>
<AnchoredOverlay
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
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}
>
<div style={{padding: '16px', width: '100%', height: '400px'}}>
<Stack gap="condensed">
<Text weight="medium">Overlay content (outside-left)</Text>
<Text>
This overlay opens to the left of the anchor on a small viewport. It should reposition to avoid
overflowing the viewport.
</Text>
</Stack>
</div>
</AnchoredOverlay>
</div>
)
},
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.',
},
},
},
}
26 changes: 6 additions & 20 deletions packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,6 @@ export type AnchoredOverlayProps = AnchoredOverlayBaseProps &
(AnchoredOverlayPropsWithAnchor | AnchoredOverlayPropsWithoutAnchor) &
Partial<Pick<PositionSettings, 'align' | 'side' | 'anchorOffset' | 'alignmentOffset' | 'displayInViewport'>>

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',
Expand Down Expand Up @@ -173,7 +162,9 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
displayCloseButton = true,
closeButtonProps = defaultCloseButtonProps,
}) => {
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<HTMLDivElement>()
const anchorId = useId(externalAnchorId)
Expand Down Expand Up @@ -232,19 +223,14 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
[overlayRef.current],
)

const hasLoadedAnchorPositioningPolyfill = useRef(false)

useEffect(() => {
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,
Expand Down
Loading