Skip to content

Commit 808cd50

Browse files
authored
feat(react-positioning): allow to configure boundaries with "PositioningRect" (#33724)
1 parent 0230517 commit 808cd50

13 files changed

+277
-20
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import * as React from 'react';
2+
import {
3+
Popover,
4+
PopoverTrigger,
5+
PopoverSurface,
6+
Button,
7+
makeStyles,
8+
tokens,
9+
type PositioningRect,
10+
useIsomorphicLayoutEffect,
11+
} from '@fluentui/react-components';
12+
13+
const useClasses = makeStyles({
14+
area: {
15+
border: `2px solid ${tokens.colorStatusDangerBackground3}`,
16+
padding: '60px 20px 20px 20px',
17+
width: '300px',
18+
height: '300px',
19+
20+
display: 'flex',
21+
flexDirection: 'column',
22+
alignItems: 'end',
23+
justifyContent: 'space-between',
24+
position: 'relative',
25+
26+
'::before': {
27+
content: '"Container"',
28+
position: 'absolute',
29+
padding: `${tokens.spacingHorizontalMNudge} ${tokens.spacingHorizontalS}`,
30+
31+
top: 0,
32+
left: 0,
33+
34+
color: tokens.colorStatusDangerBackground1,
35+
backgroundColor: tokens.colorStatusDangerBackground3,
36+
},
37+
},
38+
boundary: {
39+
width: '320px',
40+
height: '320px',
41+
outline: `2px solid ${tokens.colorBrandBackground}`,
42+
43+
position: 'absolute',
44+
top: '50px',
45+
left: '10px',
46+
pointerEvents: 'none',
47+
48+
'::before': {
49+
content: '"Boundary"',
50+
position: 'absolute',
51+
padding: `${tokens.spacingHorizontalMNudge} ${tokens.spacingHorizontalS}`,
52+
53+
top: 0,
54+
left: 0,
55+
56+
color: tokens.colorNeutralForegroundOnBrand,
57+
backgroundColor: tokens.colorBrandBackground,
58+
},
59+
},
60+
});
61+
62+
export const OverflowBoundaryRect = () => {
63+
const classes = useClasses();
64+
65+
const boundaryRef = React.useRef<HTMLDivElement | null>(null);
66+
const [boundaryRect, setBoundaryRect] = React.useState<PositioningRect | null>(null);
67+
68+
useIsomorphicLayoutEffect(() => {
69+
setBoundaryRect(boundaryRef.current?.getBoundingClientRect() ?? null);
70+
}, []);
71+
72+
return (
73+
<div className={classes.area}>
74+
<div className={classes.boundary} ref={boundaryRef} />
75+
76+
<Popover
77+
positioning={{
78+
overflowBoundary: boundaryRect,
79+
position: 'below',
80+
align: 'start',
81+
}}
82+
>
83+
<PopoverTrigger disableButtonEnhancement>
84+
<Button>
85+
<code>align: start</code>
86+
</Button>
87+
</PopoverTrigger>
88+
<PopoverSurface>Stays within the defined rect</PopoverSurface>
89+
</Popover>
90+
91+
<Popover
92+
positioning={{
93+
overflowBoundary: boundaryRect,
94+
position: 'above',
95+
align: 'start',
96+
}}
97+
>
98+
<PopoverTrigger disableButtonEnhancement>
99+
<Button>
100+
<code>align: start</code>
101+
</Button>
102+
</PopoverTrigger>
103+
<PopoverSurface>Stays within the defined rect</PopoverSurface>
104+
</Popover>
105+
</div>
106+
);
107+
};
108+
109+
OverflowBoundaryRect.parameters = {
110+
docs: {
111+
description: {
112+
story: [
113+
'Boundaries can be also defined as `Rect` objects. ',
114+
'This is useful when a boundary is not an actual element, but some kind of computed values.',
115+
].join('\n'),
116+
},
117+
},
118+
};

apps/public-docsite-v9/src/Concepts/Positioning/index.stories.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ export { AnchorToTarget } from './PositioningAnchorToTarget.stories';
1111
export { ImperativeAnchorTarget } from './PositioningImperativeAnchorTarget.stories';
1212
export { ImperativePositionUpdate } from './PositioningImperativePositionUpdate.stories';
1313
export { OverflowBoundary } from './PositioningOverflowBoundary.stories';
14-
export { OverflowBoundaryPadding } from './OverflowBoundaryPadding.stories';
14+
export { OverflowBoundaryRect } from './PositioningOverflowBoundaryRect.stories';
15+
export { OverflowBoundaryPadding } from './PositioningOverflowBoundaryPadding.stories';
1516
export { FlipBoundary } from './PositioningFlipBoundary.stories';
16-
export { MatchTargetSize } from './MatchTargetSize.stories';
17+
export { MatchTargetSize } from './PositioningMatchTargetSize.stories';
1718
export { DisableTransform } from './PositioningDisableTransform.stories';
1819
export { ListenToUpdates } from './PositioningListenToUpdates.stories';
1920
export { AutoSizeForSmallViewport } from './PositioningAutoSize.stories';

apps/vr-tests-react-components/src/stories/Positioning/Positioning.stories.tsx

+95-6
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
PositioningProps,
66
PositioningVirtualElement,
77
PositioningImperativeRef,
8+
type PositioningRect,
89
} from '@fluentui/react-positioning';
9-
import { useMergedRefs } from '@fluentui/react-utilities';
10+
import { useMergedRefs, useIsomorphicLayoutEffect } from '@fluentui/react-utilities';
1011
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
1112
import { Steps, StoryWright } from 'storywright';
1213

@@ -814,11 +815,7 @@ const PositioningEndEvent = () => {
814815
};
815816

816817
const TargetDisplayNone = () => {
817-
const positioningRef = React.useRef<PositioningImperativeRef>(null);
818-
const { targetRef, containerRef } = usePositioning({
819-
positioningRef,
820-
});
821-
818+
const { targetRef, containerRef } = usePositioning({});
822819
const [visible, setVisible] = React.useState(true);
823820

824821
return (
@@ -939,6 +936,95 @@ const ShiftToCoverTargetAsyncContent = () => {
939936
);
940937
};
941938

939+
const BoundaryRect = () => {
940+
const rectHostRef = React.useRef<HTMLDivElement>(null);
941+
942+
const boundaryRect = React.useMemo<PositioningRect>(
943+
() => ({
944+
width: 700,
945+
height: 700,
946+
x: 70,
947+
y: 70,
948+
}),
949+
[],
950+
);
951+
const { targetRef, containerRef } = usePositioning({
952+
overflowBoundary: boundaryRect,
953+
954+
position: 'below',
955+
align: 'end',
956+
});
957+
958+
useIsomorphicLayoutEffect(() => {
959+
const rectEl = document.createElement('div');
960+
961+
Object.assign(rectEl.style, {
962+
position: 'fixed',
963+
border: '4px solid orange',
964+
boxSizing: 'border-box',
965+
966+
left: `${boundaryRect.x}px`,
967+
top: `${boundaryRect.y}px`,
968+
width: `${boundaryRect.width}px`,
969+
height: `${boundaryRect.height}px`,
970+
971+
zIndex: 1,
972+
});
973+
974+
rectHostRef.current?.append(rectEl);
975+
976+
return () => {
977+
rectEl.remove();
978+
};
979+
}, [boundaryRect]);
980+
981+
return (
982+
<>
983+
<div ref={rectHostRef} />
984+
<div
985+
style={{
986+
display: 'flex',
987+
width: 800,
988+
height: 800,
989+
border: '4px dashed green',
990+
padding: 50,
991+
992+
position: 'absolute',
993+
left: 10,
994+
top: 10,
995+
}}
996+
>
997+
<div
998+
style={{
999+
padding: 20,
1000+
backgroundColor: 'lightgray',
1001+
fontSize: 20,
1002+
display: 'flex',
1003+
height: 'fit-content',
1004+
width: '100%',
1005+
}}
1006+
ref={targetRef}
1007+
>
1008+
Hello world
1009+
</div>
1010+
</div>
1011+
<div
1012+
ref={containerRef}
1013+
style={{ border: '2px solid blue', padding: 10, backgroundColor: 'white', boxSizing: 'border-box', zIndex: 2 }}
1014+
>
1015+
<ul>
1016+
<li>
1017+
SHOULD BE below gray box as it's a <code>target</code>
1018+
</li>
1019+
<li>
1020+
SHOULD BE inside an orange box as it's a <code>overflowBoundary</code>
1021+
</li>
1022+
</ul>
1023+
</div>
1024+
</>
1025+
);
1026+
};
1027+
9421028
export default {
9431029
title: 'Positioning',
9441030

@@ -960,6 +1046,9 @@ export default {
9601046
],
9611047
} satisfies Meta<'div'>;
9621048

1049+
export const _BoundaryRect = () => <BoundaryRect />;
1050+
_BoundaryRect.storyName = 'using boundary rect';
1051+
9631052
export const _PositionAndAlignProps = () => <PositionAndAlignProps />;
9641053
_PositionAndAlignProps.storyName = 'position and align props';
9651054

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "feat: export \"PositioningBoundary\" & \"PositioningRect\" types",
4+
"packageName": "@fluentui/react-components",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "feat: allow to configure boundaries with \"PositioningRect\"",
4+
"packageName": "@fluentui/react-positioning",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/react-components/react-components/etc/react-components.api.md

+6
Original file line numberDiff line numberDiff line change
@@ -691,8 +691,10 @@ import { Portal } from '@fluentui/react-portal';
691691
import { PortalMountNodeProvider } from '@fluentui/react-shared-contexts';
692692
import { PortalProps } from '@fluentui/react-portal';
693693
import { PortalState } from '@fluentui/react-portal';
694+
import { PositioningBoundary } from '@fluentui/react-positioning';
694695
import { PositioningImperativeRef } from '@fluentui/react-positioning';
695696
import { PositioningProps } from '@fluentui/react-positioning';
697+
import { PositioningRect } from '@fluentui/react-positioning';
696698
import { PositioningShorthand } from '@fluentui/react-positioning';
697699
import { PositioningShorthandValue } from '@fluentui/react-positioning';
698700
import { PositioningVirtualElement } from '@fluentui/react-positioning';
@@ -3201,10 +3203,14 @@ export { PortalProps }
32013203

32023204
export { PortalState }
32033205

3206+
export { PositioningBoundary }
3207+
32043208
export { PositioningImperativeRef }
32053209

32063210
export { PositioningProps }
32073211

3212+
export { PositioningRect }
3213+
32083214
export { PositioningShorthand }
32093215

32103216
export { PositioningShorthandValue }

packages/react-components/react-components/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,9 @@ export type {
821821

822822
export { resolvePositioningShorthand } from '@fluentui/react-positioning';
823823
export type {
824+
PositioningBoundary,
824825
PositioningProps,
826+
PositioningRect,
825827
PositioningShorthand,
826828
PositioningShorthandValue,
827829
PositioningImperativeRef,

packages/react-components/react-positioning/etc/react-positioning.api.md

+15-4
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export type Alignment = 'top' | 'bottom' | 'start' | 'end' | 'center';
1313
// @public (undocumented)
1414
export type AutoSize = 'height' | 'height-always' | 'width' | 'width-always' | 'always' | boolean;
1515

16-
// @public (undocumented)
17-
export type Boundary = HTMLElement | Array<HTMLElement> | 'clippingParents' | 'scrollParent' | 'window';
16+
// @public @deprecated (undocumented)
17+
export type Boundary = PositioningBoundary;
1818

1919
// @internal
2020
export function createArrowHeightStyles(arrowHeight: number): GriffelStyle;
@@ -47,8 +47,8 @@ export type OffsetFunction = (param: OffsetFunctionParam) => OffsetObject | Offs
4747

4848
// @public (undocumented)
4949
export type OffsetFunctionParam = {
50-
positionedRect: Rect;
51-
targetRect: Rect;
50+
positionedRect: PositioningRect;
51+
targetRect: PositioningRect;
5252
position: Position;
5353
alignment?: Alignment;
5454
};
@@ -65,6 +65,9 @@ export type OffsetShorthand = number;
6565
// @public (undocumented)
6666
export type Position = 'above' | 'below' | 'before' | 'after';
6767

68+
// @public (undocumented)
69+
export type PositioningBoundary = PositioningRect | HTMLElement | Array<HTMLElement> | 'clippingParents' | 'scrollParent' | 'window';
70+
6871
// @public (undocumented)
6972
export type PositioningImperativeRef = {
7073
updatePosition: () => void;
@@ -77,6 +80,14 @@ export interface PositioningProps extends Pick<PositioningOptions, 'align' | 'ar
7780
target?: TargetElement | null;
7881
}
7982

83+
// @public (undocumented)
84+
export type PositioningRect = {
85+
width: number;
86+
height: number;
87+
x: number;
88+
y: number;
89+
};
90+
8091
// @public (undocumented)
8192
export type PositioningShorthand = PositioningProps | PositioningShorthandValue;
8293

packages/react-components/react-positioning/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ export { resolvePositioningShorthand, mergeArrowOffset } from './utils/index';
88
export type {
99
Alignment,
1010
AutoSize,
11+
// eslint-disable-next-line @typescript-eslint/no-deprecated
1112
Boundary,
1213
Offset,
1314
OffsetFunction,
1415
OffsetFunctionParam,
1516
OffsetObject,
1617
OffsetShorthand,
1718
Position,
19+
PositioningBoundary,
1820
PositioningImperativeRef,
1921
PositioningProps,
22+
PositioningRect,
2023
PositioningShorthand,
2124
PositioningShorthandValue,
2225
PositioningVirtualElement,

0 commit comments

Comments
 (0)