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
5 changes: 5 additions & 0 deletions .changeset/soft-pianos-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

AnchoredOverlay: Add Popover API to AnchoredOverlay (behind `primer_react_css_anchor_positioning` feature flag)
45 changes: 34 additions & 11 deletions e2e/components/AnchoredOverlay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const stories: Array<{
viewport?: keyof typeof viewports
waitForText?: string
buttonName?: string
buttonNames?: string[]
openDialog?: boolean
openNestedDialog?: boolean
}> = [
Expand Down Expand Up @@ -97,6 +98,11 @@ const stories: Array<{
// buttonName: 'Open Overlay',
// openDialog: true,
// },
{
title: 'Multiple Overlays',
id: 'components-anchoredoverlay-features--multiple-overlays',
buttonNames: ['renderAnchor 1', 'External anchor 1', 'renderAnchor 2', 'External anchor 2'],
},
{
title: 'Within Sticky Element',
id: 'components-anchoredoverlay-features--within-sticky-element',
Expand Down Expand Up @@ -153,19 +159,36 @@ test.describe('AnchoredOverlay', () => {
await page.getByRole('button', {name: 'Open Inner Dialog'}).click()
}

// Open the overlay
const buttonName = story.buttonName ?? 'Button'
await page.locator('button', {hasText: buttonName}).first().waitFor()
const overlayButton = page.getByRole('button', {name: buttonName}).first()
await overlayButton.click()
// If the story has multiple overlays, screenshot each one individually
if (story.buttonNames) {
for (const name of story.buttonNames) {
await page.locator('button', {hasText: name}).first().waitFor()
const btn = page.getByRole('button', {name}).first()
await btn.click()
await waitForImages(page)

expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`AnchoredOverlay.${story.title}.${name}.${theme}${namePostfix}.png`,
)

// for the dev stories, we intentionally change the content after the overlay is open to test that it repositions correctly
if (story.waitForText) await page.getByText(story.waitForText).waitFor()
await waitForImages(page)
// Close the overlay before opening the next one
await btn.click()
}
} else {
// Open the overlay
const buttonName = story.buttonName ?? 'Button'
await page.locator('button', {hasText: buttonName}).first().waitFor()
const overlayButton = page.getByRole('button', {name: buttonName}).first()
await overlayButton.click()

expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`AnchoredOverlay.${story.title}.${theme}${namePostfix}.png`,
)
// for the dev stories, we intentionally change the content after the overlay is open to test that it repositions correctly
if (story.waitForText) await page.getByText(story.waitForText).waitFor()
await waitForImages(page)

expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`AnchoredOverlay.${story.title}.${theme}${namePostfix}.png`,
)
}
})
}
})
Expand Down
5 changes: 0 additions & 5 deletions packages/react/src/ActionMenu/ActionMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {Tooltip as TooltipV2} from '../TooltipV2/Tooltip'
import {SingleSelect} from '../ActionMenu/ActionMenu.features.stories'
import {MixedSelection} from '../ActionMenu/ActionMenu.examples.stories'
import {SearchIcon, KebabHorizontalIcon} from '@primer/octicons-react'
import anchoredOverlayClasses from '../AnchoredOverlay/AnchoredOverlay.module.css'
import {getAnchoredPosition} from '@primer/behaviors'
import type {AnchorPosition} from '@primer/behaviors'

Expand Down Expand Up @@ -651,7 +650,6 @@ describe('ActionMenu', () => {
)
const anchor = component.getByRole('button', {name: 'Toggle Menu'})
expect(anchor).toHaveClass('test-class')
expect(anchor).toHaveClass(anchoredOverlayClasses.Anchor)
})

it('supports className prop on ActionMenu.Button with css anchor positioning flag', async () => {
Expand Down Expand Up @@ -680,7 +678,6 @@ describe('ActionMenu', () => {
)
const button = component.getByRole('button', {name: 'Toggle Menu'})
expect(button).toHaveClass('test-class')
expect(button).toHaveClass(anchoredOverlayClasses.Anchor)
})

it('supports className prop on ActionMenu.Anchor', async () => {
Expand Down Expand Up @@ -711,7 +708,6 @@ describe('ActionMenu', () => {
)
const anchor = component.getByRole('button', {name: 'Toggle Menu'})
expect(anchor).toHaveClass('test-class')
expect(anchor).not.toHaveClass(anchoredOverlayClasses.Anchor)
})

it('supports className prop on ActionMenu.Button', async () => {
Expand Down Expand Up @@ -740,7 +736,6 @@ describe('ActionMenu', () => {
)
const button = component.getByRole('button', {name: 'Toggle Menu'})
expect(button).toHaveClass('test-class')
expect(button).not.toHaveClass(anchoredOverlayClasses.Anchor)
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,95 @@ export const WithinDialogOverflowing = () => {
)
}

export const MultipleOverlays = () => {
const [openOverlay, setOpenOverlay] = useState<string | null>(null)
const externalAnchorRefA = useRef<HTMLButtonElement>(null)
const externalAnchorRefB = useRef<HTMLButtonElement>(null)

const open = (key: string) => () => setOpenOverlay(key)
const close = () => setOpenOverlay(null)

return (
<Stack direction="horizontal" gap="normal" align="start" style={{padding: '16px'}}>
<AnchoredOverlay
open={openOverlay === 'render-1'}
onOpen={open('render-1')}
onClose={close}
renderAnchor={props => <Button {...props}>renderAnchor 1</Button>}
overlayProps={{
role: 'dialog',
'aria-modal': true,
'aria-label': 'Overlay 1',
}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<div className={classes.FlexColFill}>{hoverCard}</div>
</AnchoredOverlay>

<Button
ref={externalAnchorRefA}
onClick={() => setOpenOverlay(openOverlay === 'external-1' ? null : 'external-1')}
>
External anchor 1
</Button>
<AnchoredOverlay
open={openOverlay === 'external-1'}
onClose={close}
renderAnchor={null}
anchorRef={externalAnchorRefA}
overlayProps={{
role: 'dialog',
'aria-modal': true,
'aria-label': 'Overlay 2',
}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<div className={classes.FlexColFill}>{hoverCard}</div>
</AnchoredOverlay>

<AnchoredOverlay
open={openOverlay === 'render-2'}
onOpen={open('render-2')}
onClose={close}
renderAnchor={props => <Button {...props}>renderAnchor 2</Button>}
overlayProps={{
role: 'dialog',
'aria-modal': true,
'aria-label': 'Overlay 3',
}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<div className={classes.FlexColFill}>{hoverCard}</div>
</AnchoredOverlay>

<Button
ref={externalAnchorRefB}
onClick={() => setOpenOverlay(openOverlay === 'external-2' ? null : 'external-2')}
>
External anchor 2
</Button>
<AnchoredOverlay
open={openOverlay === 'external-2'}
onClose={close}
renderAnchor={null}
anchorRef={externalAnchorRefB}
overlayProps={{
role: 'dialog',
'aria-modal': true,
'aria-label': 'Overlay 4',
}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<div className={classes.FlexColFill}>{hoverCard}</div>
</AnchoredOverlay>
</Stack>
)
}

export const WithinStickyElement = () => {
return (
<div className={classes.ScrollContainer}>
Expand Down
22 changes: 10 additions & 12 deletions packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,23 @@
}
}

.Wrapper {
anchor-scope: --anchored-overlay-anchor;
}

.Anchor {
/* Anchor name, this is currently tied to `renderAnchor` */
anchor-name: --anchored-overlay-anchor;
}

.AnchoredOverlay {
/* Anchor position, this is currently tied to `<Overlay>` */
position-anchor: --anchored-overlay-anchor;
position-try-fallbacks:
flip-block,
flip-inline,
flip-block flip-inline;
position-visibility: anchors-visible;
z-index: 100;
position: fixed;
position: fixed !important;

&[popover] {
inset: auto;
margin: 0;
padding: 0;
border: 0;
max-height: none;
max-width: none;
}

&[data-side='outside-bottom'] {
/* stylelint-disable primer/spacing */
Expand Down
Loading
Loading