Skip to content

Commit dec8173

Browse files
authored
feat(popover): add autoAlignBoundary for configurable collision boundary (#16995)
* feat(popover): add autoAlignBoundary for configurable collision boundary * chore: yarn format * fix(popover): improve autoAlignBoundary proptype * fix(popover): ensure popover conforms to popovercomponent * chore: update snaps
1 parent 140794c commit dec8173

File tree

3 files changed

+203
-1
lines changed

3 files changed

+203
-1
lines changed

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6273,6 +6273,55 @@ Map {
62736273
"autoAlign": Object {
62746274
"type": "bool",
62756275
},
6276+
"autoAlignBoundary": Object {
6277+
"args": Array [
6278+
Array [
6279+
Object {
6280+
"args": Array [
6281+
Array [
6282+
"clippingAncestors",
6283+
],
6284+
],
6285+
"type": "oneOf",
6286+
},
6287+
Object {
6288+
"type": "elementType",
6289+
},
6290+
Object {
6291+
"args": Array [
6292+
Object {
6293+
"type": "elementType",
6294+
},
6295+
],
6296+
"type": "arrayOf",
6297+
},
6298+
Object {
6299+
"args": Array [
6300+
Object {
6301+
"height": Object {
6302+
"isRequired": true,
6303+
"type": "number",
6304+
},
6305+
"width": Object {
6306+
"isRequired": true,
6307+
"type": "number",
6308+
},
6309+
"x": Object {
6310+
"isRequired": true,
6311+
"type": "number",
6312+
},
6313+
"y": Object {
6314+
"isRequired": true,
6315+
"type": "number",
6316+
},
6317+
},
6318+
],
6319+
"type": "exact",
6320+
},
6321+
],
6322+
],
6323+
"type": "oneOfType",
6324+
},
62766325
"caret": Object {
62776326
"type": "bool",
62786327
},

packages/react/src/components/Popover/Popover.stories.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,136 @@ export const ExperimentalAutoAlign = () => {
270270
);
271271
};
272272

273+
export const ExperimentalAutoAlignBoundary = () => {
274+
const [open, setOpen] = useState(true);
275+
const ref = useRef();
276+
const [boundary, setBoundary] = useState();
277+
278+
useEffect(() => {
279+
ref?.current?.scrollIntoView({ block: 'center', inline: 'center' });
280+
});
281+
282+
return (
283+
<div
284+
style={{
285+
display: 'grid',
286+
placeItems: 'center',
287+
overflow: 'scroll',
288+
width: '800px',
289+
height: '500px',
290+
border: '1px',
291+
borderStyle: 'dashed',
292+
borderColor: 'black',
293+
margin: '0 auto',
294+
}}
295+
ref={setBoundary}>
296+
<div
297+
style={{
298+
width: '2100px',
299+
height: '1px',
300+
placeItems: 'center',
301+
}}
302+
/>
303+
<div style={{ placeItems: 'center', height: '32px', width: '32px' }}>
304+
<Popover
305+
open={open}
306+
align="top"
307+
autoAlign
308+
autoAlignBoundary={boundary}
309+
ref={ref}>
310+
<div className="playground-trigger">
311+
<CheckboxIcon
312+
onClick={() => {
313+
setOpen(!open);
314+
}}
315+
/>
316+
</div>
317+
<PopoverContent className="p-3">
318+
<div>
319+
<p className="popover-title">This popover uses autoAlign</p>
320+
<p className="popover-details">
321+
Scroll the container up, down, left or right to observe how the
322+
popover will automatically change its position in attempt to
323+
stay within the viewport. This works on initial render in
324+
addition to on scroll.
325+
</p>
326+
</div>
327+
</PopoverContent>
328+
</Popover>
329+
<div
330+
style={{
331+
height: '1000px',
332+
width: '1px',
333+
placeItems: 'center',
334+
}}
335+
/>
336+
</div>
337+
</div>
338+
);
339+
};
340+
341+
export const Test = () => {
342+
const [open, setOpen] = useState();
343+
const align = document?.dir === 'rtl' ? 'bottom-right' : 'bottom-left';
344+
const alignTwo = document?.dir === 'rtl' ? 'bottom-left' : 'bottom-right';
345+
return (
346+
<div style={{ display: 'flex', gap: '8rem' }}>
347+
<OverflowMenu
348+
flipped={document?.dir === 'rtl'}
349+
aria-label="overflow-menu">
350+
<OverflowMenuItem itemText="Stop app" />
351+
<OverflowMenuItem itemText="Restart app" />
352+
<OverflowMenuItem itemText="Rename app" />
353+
<OverflowMenuItem itemText="Clone and move app" disabled requireTitle />
354+
<OverflowMenuItem itemText="Edit routes and access" requireTitle />
355+
<OverflowMenuItem hasDivider isDelete itemText="Delete app" />
356+
</OverflowMenu>
357+
358+
<Popover
359+
align={align}
360+
open={open}
361+
onKeyDown={(evt) => {
362+
if (match(evt, keys.Escape)) {
363+
setOpen(false);
364+
}
365+
}}
366+
isTabTip
367+
onRequestClose={() => setOpen(false)}>
368+
<button
369+
aria-label="Settings"
370+
type="button"
371+
aria-expanded={open}
372+
onClick={() => {
373+
setOpen(!open);
374+
}}>
375+
<Settings />
376+
</button>
377+
<PopoverContent className="p-3">
378+
<RadioButtonGroup
379+
style={{ alignItems: 'flex-start', flexDirection: 'column' }}
380+
legendText="Row height"
381+
name="radio-button-group"
382+
defaultSelected="small">
383+
<RadioButton labelText="Small" value="small" id="radio-small" />
384+
<RadioButton labelText="Large" value="large" id="radio-large" />
385+
</RadioButtonGroup>
386+
<hr />
387+
<fieldset className={`cds--fieldset`}>
388+
<legend className={`cds--label`}>Edit columns</legend>
389+
<Checkbox defaultChecked labelText="Name" id="checkbox-label-1" />
390+
<Checkbox defaultChecked labelText="Type" id="checkbox-label-2" />
391+
<Checkbox
392+
defaultChecked
393+
labelText="Location"
394+
id="checkbox-label-3"
395+
/>
396+
</fieldset>
397+
</PopoverContent>
398+
</Popover>
399+
</div>
400+
);
401+
};
402+
273403
export const TabTipExperimentalAutoAlign = () => {
274404
const [open, setOpen] = useState(true);
275405
const ref = useRef();

packages/react/src/components/Popover/index.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
autoUpdate,
2929
arrow,
3030
offset,
31+
type Boundary,
3132
} from '@floating-ui/react';
3233
import { hide } from '@floating-ui/dom';
3334
import { useFeatureFlag } from '../FeatureFlags';
@@ -102,6 +103,11 @@ interface PopoverBaseProps {
102103
*/
103104
autoAlign?: boolean;
104105

106+
/**
107+
* 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.
108+
*/
109+
autoAlignBoundary?: Boundary;
110+
105111
/**
106112
* Specify whether a caret should be rendered
107113
*/
@@ -165,6 +171,7 @@ export const Popover: PopoverComponent = React.forwardRef(
165171
align: initialAlign = isTabTip ? 'bottom-start' : 'bottom',
166172
as: BaseComponent = 'span' as E,
167173
autoAlign = false,
174+
autoAlignBoundary,
168175
caret = isTabTip ? false : true,
169176
className: customClassName,
170177
children,
@@ -292,6 +299,7 @@ export const Popover: PopoverComponent = React.forwardRef(
292299

293300
fallbackStrategy: 'initialPlacement',
294301
fallbackAxisSideDirection: 'start',
302+
boundary: autoAlignBoundary,
295303
}),
296304
arrow({
297305
element: caretRef,
@@ -476,7 +484,7 @@ export const Popover: PopoverComponent = React.forwardRef(
476484
</PopoverContext.Provider>
477485
);
478486
}
479-
);
487+
) as PopoverComponent;
480488

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

557+
/**
558+
* 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.
559+
*/
560+
autoAlignBoundary: PropTypes.oneOfType([
561+
PropTypes.oneOf(['clippingAncestors']),
562+
PropTypes.elementType,
563+
PropTypes.arrayOf(PropTypes.elementType),
564+
PropTypes.exact({
565+
x: PropTypes.number.isRequired,
566+
y: PropTypes.number.isRequired,
567+
width: PropTypes.number.isRequired,
568+
height: PropTypes.number.isRequired,
569+
}),
570+
]) as PropTypes.Validator<Boundary | null | undefined>,
571+
549572
/**
550573
* Specify whether a caret should be rendered
551574
*/

0 commit comments

Comments
 (0)