Skip to content

feat(AnchoredOverlay): allow overlay to reflow #5210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
5 changes: 5 additions & 0 deletions .changeset/thick-pugs-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

feat(AnchoredOverlay): allow overlay to reflow
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
"type": "string",
"defaultValue": "",
"description": "Class name for custom styling."
},
{
"name": "preventOverflow",
"type": "boolean",
"defaultValue": "true",
"description": "Determines if the Overlay width should be adjusted responsively if there is not enough space to display the Overlay. If `preventOverflow` is set to `false`, the Overlay will be displayed at the maximum width that fits within the viewport."
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default {
} as Meta

const hoverCard = (
<Stack gap="condensed" style={{minWidth: '320px', padding: '16px'}}>
<Stack gap="condensed" style={{padding: '16px'}}>
<Stack direction="horizontal" gap="condensed" justify="space-between">
<Avatar src="https://avatars.githubusercontent.com/u/92997159?v=4" size={48} />
<Button size="small">Follow</Button>
Expand Down Expand Up @@ -103,8 +103,9 @@ export const CustomAnchorId = () => {
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
anchorId="my-custom-anchor-id"
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay'}}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay', sx: {minWidth: '320px'}}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<Box sx={{width: '100%', height: '100%', display: 'flex', flexDirection: 'column'}}>{hoverCard}</Box>
</AnchoredOverlay>
Expand All @@ -121,8 +122,9 @@ export const Height = () => {
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
height="large"
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay'}}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay', sx: {minWidth: '320px'}}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<Box sx={{width: '100%', height: '100%', display: 'flex', flexDirection: 'column'}}>{hoverCard}</Box>
</AnchoredOverlay>
Expand All @@ -139,8 +141,9 @@ export const Width = () => {
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
width="large"
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay'}}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay', sx: {minWidth: '320px'}}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<Box
sx={{
Expand Down Expand Up @@ -170,8 +173,9 @@ export const AnchorAlignment = () => {
</Button>
)}
align="center"
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay'}}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay', sx: {minWidth: '320px'}}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<Box sx={{width: '100%', height: '100%', display: 'flex', flexDirection: 'column'}}>{hoverCard}</Box>
</AnchoredOverlay>
Expand All @@ -188,8 +192,9 @@ export const AnchorSide = () => {
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
side="outside-right"
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay'}}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay', sx: {minWidth: '320px'}}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<Box sx={{width: '100%', height: '100%', display: 'flex', flexDirection: 'column'}}>{hoverCard}</Box>
</AnchoredOverlay>
Expand All @@ -206,8 +211,9 @@ export const OffsetPositionFromAnchor = () => {
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
anchorOffset={100}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay'}}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay', sx: {minWidth: '320px'}}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<Box sx={{width: '100%', height: '100%', display: 'flex', flexDirection: 'column'}}>{hoverCard}</Box>
</AnchoredOverlay>
Expand All @@ -224,8 +230,9 @@ export const OffsetAlignmentFromAnchor = () => {
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
alignmentOffset={100}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay'}}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay', sx: {minWidth: '320px'}}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<Box sx={{width: '100%', height: '100%', display: 'flex', flexDirection: 'column'}}>{hoverCard}</Box>
</AnchoredOverlay>
Expand All @@ -245,6 +252,7 @@ export const FocusTrapOverrides = () => {
focusTrapSettings={{initialFocusRef}}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'Focus Trap Demo Overlay'}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<Button>First button</Button>
<Button ref={initialFocusRef}>Initial focus</Button>
Expand All @@ -263,6 +271,7 @@ export const FocusZoneOverrides = () => {
renderAnchor={props => <Button {...props}>Button</Button>}
focusZoneSettings={{bindKeys: FocusKeys.JK}}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'Focus Zone Demo Overlay'}}
preventOverflow={false}
>
<p>
Use <kbd>J</kbd> and <kbd>K</kbd> keys to move focus.
Expand All @@ -289,8 +298,10 @@ export const OverlayPropsOverrides = () => {
role: 'dialog',
'aria-modal': true,
'aria-label': 'User Card Overlay',
sx: {minWidth: '320px'},
}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
<div>Overlay props have been overridden to set: </div>
<pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default {
} as Meta

const hoverCard = (
<Stack gap="condensed" style={{minWidth: '320px', padding: '16px'}}>
<Stack gap="condensed" style={{padding: '16px'}}>
<Stack direction="horizontal" gap="condensed" justify="space-between">
<Avatar src="https://avatars.githubusercontent.com/u/92997159?v=4" size={48} />
<Button size="small">Follow</Button>
Expand Down Expand Up @@ -53,8 +53,9 @@ export const Default = () => {
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
renderAnchor={props => <Button {...props}>Button</Button>}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay'}}
overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay', sx: {minWidth: '320px'}}}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
{hoverCard}
</AnchoredOverlay>
Expand Down Expand Up @@ -83,9 +84,11 @@ export const Playground = (args: Args) => {
role: 'dialog',
'aria-modal': true,
'aria-label': 'User Card Overlay',
sx: {minWidth: '320px'},
}}
side={args.side}
focusZoneSettings={{disabled: true}}
preventOverflow={false}
>
{hoverCard}
</AnchoredOverlay>
Expand Down
8 changes: 7 additions & 1 deletion packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ interface AnchoredOverlayBaseProps extends Pick<OverlayProps, 'height' | 'width'
* Optional className to be added to the overlay component.
*/
className?: string
/**
* preventOverflow Optional. The Overlay width will be adjusted responsively if there is not enough space to display the Overlay.
* If `preventOverflow` is `true`, the width of the `Overlay` will not be adjusted.
*/
preventOverflow?: boolean
}

export type AnchoredOverlayProps = AnchoredOverlayBaseProps &
Expand Down Expand Up @@ -112,6 +117,7 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
alignmentOffset,
anchorOffset,
className,
preventOverflow = true,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On by default, which means the overlay will NOT reflow unless preventOverflow={false} is explicitly set as a prop on the component. This is of course overriden by the primer_react_overlay_overflow FF introduced in https://github.com/primer/react/pull/5129/files#diff-228ff69d3a08b1a41b50995af899947232eb64943d74ffd7aefd6bdd80aac449R9

}) => {
const anchorRef = useProvidedRefOrCreate(externalAnchorRef)
const [overlayRef, updateOverlayRef] = useRenderForcingRef<HTMLDivElement>()
Expand Down Expand Up @@ -198,7 +204,7 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
left={position?.left || 0}
anchorSide={position?.anchorSide}
className={className}
preventOverflow={true}
preventOverflow={preventOverflow}
{...overlayProps}
>
{children}
Expand Down
14 changes: 0 additions & 14 deletions packages/react/src/Overlay/Overlay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,18 +313,4 @@ describe('Overlay', () => {
const container = getByRole('none')
expect(container).toHaveAttribute('data-reflow-container')
})

it('should not have `data-reflow-container` if FF is enabled but the overlay is above `medium`', async () => {
const user = userEvent.setup()
const {getByRole} = render(
<FeatureFlags flags={{primer_react_overlay_overflow: true}}>
<TestComponent width="large" />
</FeatureFlags>,
)

await user.click(getByRole('button', {name: 'open overlay'}))

const container = getByRole('none')
expect(container).not.toHaveAttribute('data-reflow-container')
})
})
6 changes: 2 additions & 4 deletions packages/react/src/Overlay/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ type OwnOverlayProps = Merge<StyledOverlayProps, BaseOverlayProps>
* @param bottom Optional. Vertical bottom position of the overlay, relative to its closest positioned ancestor (often its `Portal`).
* @param position Optional. Sets how an element is positioned in a document. Defaults to `absolute` positioning.
* @param portalContainerName Optional. The name of the portal container to render the Overlay into.
* @param preventOverflow Optional. The Overlay width will be adjusted responsively if width is `auto`, `medium` or lower and there is not enough space to display the Overlay. If `preventOverflow` is `true`, the width of the `Overlay` will not be adjusted.
* @param preventOverflow Optional. The Overlay width will be adjusted responsively if there is not enough space to display the Overlay. If `preventOverflow` is `true`, the width of the `Overlay` will not be adjusted.
*/
const Overlay = React.forwardRef<HTMLDivElement, OwnOverlayProps>(
(
Expand Down Expand Up @@ -207,10 +207,8 @@ const Overlay = React.forwardRef<HTMLDivElement, OwnOverlayProps>(

// To be backwards compatible with the old Overlay, we need to set the left prop if x-position is not specified
const leftPosition: React.CSSProperties = left === undefined && right === undefined ? {left: 0} : {left}
const reflowSize = ['xsmall', 'small', 'medium', 'auto'].includes(width)

const enabled = useFeatureFlag('primer_react_overlay_overflow')
const overflow = enabled && reflowSize ? true : undefined

return (
<Portal containerName={portalContainerName}>
Expand All @@ -231,7 +229,7 @@ const Overlay = React.forwardRef<HTMLDivElement, OwnOverlayProps>(
...styleFromProps,
} as React.CSSProperties
}
data-reflow-container={overflow || (reflowSize && !preventOverflow) ? true : undefined}
data-reflow-container={enabled || !preventOverflow ? true : undefined}
/>
</Portal>
)
Expand Down
Loading