Skip to content
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

feat(popover): add autoAlignBoundary for configurable collision boundary #16995

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
49 changes: 49 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6251,6 +6251,55 @@ Map {
"autoAlign": Object {
"type": "bool",
},
"autoAlignBoundary": Object {
"args": Array [
Array [
Object {
"args": Array [
Array [
"clippingAncestors",
],
],
"type": "oneOf",
},
Object {
"type": "elementType",
},
Object {
"args": Array [
Object {
"type": "elementType",
},
],
"type": "arrayOf",
},
Object {
"args": Array [
Object {
"height": Object {
"isRequired": true,
"type": "number",
},
"width": Object {
"isRequired": true,
"type": "number",
},
"x": Object {
"isRequired": true,
"type": "number",
},
"y": Object {
"isRequired": true,
"type": "number",
},
},
],
"type": "exact",
},
],
],
"type": "oneOfType",
},
"caret": Object {
"type": "bool",
},
Expand Down
130 changes: 130 additions & 0 deletions packages/react/src/components/Popover/Popover.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,136 @@ export const ExperimentalAutoAlign = () => {
);
};

export const ExperimentalAutoAlignBoundary = () => {
const [open, setOpen] = useState(true);
const ref = useRef();
const [boundary, setBoundary] = useState();

useEffect(() => {
ref?.current?.scrollIntoView({ block: 'center', inline: 'center' });
});

return (
<div
style={{
display: 'grid',
placeItems: 'center',
overflow: 'scroll',
width: '800px',
height: '500px',
border: '1px',
borderStyle: 'dashed',
borderColor: 'black',
margin: '0 auto',
}}
ref={setBoundary}>
Copy link
Member Author

Choose a reason for hiding this comment

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

TIL how this pattern works: https://www.youtube.com/watch?v=leEdHQh6nP8

<div
style={{
width: '2100px',
height: '1px',
placeItems: 'center',
}}
/>
<div style={{ placeItems: 'center', height: '32px', width: '32px' }}>
<Popover
open={open}
align="top"
autoAlign
autoAlignBoundary={boundary}
ref={ref}>
<div className="playground-trigger">
<CheckboxIcon
onClick={() => {
setOpen(!open);
}}
/>
</div>
<PopoverContent className="p-3">
<div>
<p className="popover-title">This popover uses autoAlign</p>
<p className="popover-details">
Scroll the container up, down, left or right to observe how the
popover will automatically change its position in attempt to
stay within the viewport. This works on initial render in
addition to on scroll.
</p>
</div>
</PopoverContent>
</Popover>
<div
style={{
height: '1000px',
width: '1px',
placeItems: 'center',
}}
/>
</div>
</div>
);
};

export const Test = () => {
const [open, setOpen] = useState();
const align = document?.dir === 'rtl' ? 'bottom-right' : 'bottom-left';
const alignTwo = document?.dir === 'rtl' ? 'bottom-left' : 'bottom-right';
return (
<div style={{ display: 'flex', gap: '8rem' }}>
<OverflowMenu
flipped={document?.dir === 'rtl'}
aria-label="overflow-menu">
<OverflowMenuItem itemText="Stop app" />
<OverflowMenuItem itemText="Restart app" />
<OverflowMenuItem itemText="Rename app" />
<OverflowMenuItem itemText="Clone and move app" disabled requireTitle />
<OverflowMenuItem itemText="Edit routes and access" requireTitle />
<OverflowMenuItem hasDivider isDelete itemText="Delete app" />
</OverflowMenu>

<Popover
align={align}
open={open}
onKeyDown={(evt) => {
if (match(evt, keys.Escape)) {
setOpen(false);
}
}}
isTabTip
onRequestClose={() => setOpen(false)}>
<button
aria-label="Settings"
type="button"
aria-expanded={open}
onClick={() => {
setOpen(!open);
}}>
<Settings />
</button>
<PopoverContent className="p-3">
<RadioButtonGroup
style={{ alignItems: 'flex-start', flexDirection: 'column' }}
legendText="Row height"
name="radio-button-group"
defaultSelected="small">
<RadioButton labelText="Small" value="small" id="radio-small" />
<RadioButton labelText="Large" value="large" id="radio-large" />
</RadioButtonGroup>
<hr />
<fieldset className={`cds--fieldset`}>
<legend className={`cds--label`}>Edit columns</legend>
<Checkbox defaultChecked labelText="Name" id="checkbox-label-1" />
<Checkbox defaultChecked labelText="Type" id="checkbox-label-2" />
<Checkbox
defaultChecked
labelText="Location"
id="checkbox-label-3"
/>
</fieldset>
</PopoverContent>
</Popover>
</div>
);
};

export const TabTipExperimentalAutoAlign = () => {
const [open, setOpen] = useState(true);
const ref = useRef();
Expand Down
25 changes: 24 additions & 1 deletion packages/react/src/components/Popover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
autoUpdate,
arrow,
offset,
type Boundary,
} from '@floating-ui/react';
import { hide } from '@floating-ui/dom';
import { useFeatureFlag } from '../FeatureFlags';
Expand Down Expand Up @@ -102,6 +103,11 @@ interface PopoverBaseProps {
*/
autoAlign?: boolean;

/**
* Specify a bounding element to be used for autoAlign calculations. The viewport is used by default. This prop is currently experimental and is subject to future changes.
*/
autoAlignBoundary?: Boundary;

/**
* Specify whether a caret should be rendered
*/
Expand Down Expand Up @@ -165,6 +171,7 @@ export const Popover: PopoverComponent = React.forwardRef(
align: initialAlign = isTabTip ? 'bottom-start' : 'bottom',
as: BaseComponent = 'span' as E,
autoAlign = false,
autoAlignBoundary,
caret = isTabTip ? false : true,
className: customClassName,
children,
Expand Down Expand Up @@ -292,6 +299,7 @@ export const Popover: PopoverComponent = React.forwardRef(

fallbackStrategy: 'initialPlacement',
fallbackAxisSideDirection: 'start',
boundary: autoAlignBoundary,
}),
arrow({
element: caretRef,
Expand Down Expand Up @@ -476,7 +484,7 @@ export const Popover: PopoverComponent = React.forwardRef(
</PopoverContext.Provider>
);
}
);
) as PopoverComponent;

// Note: this displayName is temporarily set so that Storybook ArgTable
// correctly displays the name of this component
Expand Down Expand Up @@ -546,6 +554,21 @@ Popover.propTypes = {
*/
autoAlign: PropTypes.bool,

/**
* Specify a bounding element to be used for autoAlign calculations. The viewport is used by default. This prop is currently experimental and is subject to future changes.
*/
autoAlignBoundary: PropTypes.oneOfType([
PropTypes.oneOf(['clippingAncestors']),
PropTypes.elementType,
PropTypes.arrayOf(PropTypes.elementType),
PropTypes.exact({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
}),
]) as PropTypes.Validator<Boundary | null | undefined>,

/**
* Specify whether a caret should be rendered
*/
Expand Down
Loading