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(react-positioning): simplify maxSize options #28649

Merged
merged 14 commits into from
Jul 27, 2023
121 changes: 121 additions & 0 deletions apps/vr-tests-react-components/src/stories/Positioning.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,110 @@ const AutoSize = () => {
);
};

const AutoSizeAsyncContent = () => {
const styles = useStyles();
const [overflowBoundary, setOverflowBoundary] = React.useState<HTMLDivElement | null>(null);
const { containerRef, targetRef } = usePositioning({
position: 'below',
autoSize: true,
overflowBoundary,
});

const [isLoaded, setLoaded] = React.useState(false);
React.useEffect(() => {
setTimeout(() => {
setLoaded(true);
}, 500);
});

return (
<div
ref={setOverflowBoundary}
className={styles.boundary}
style={{
display: 'flex',
flexDirection: 'column',
height: 200,
padding: '10px 50px',
position: 'relative',
}}
>
<button ref={targetRef}>Target</button>
<Box ref={containerRef} style={{ overflow: 'auto', border: '3px solid green' }}>
{isLoaded ? (
<span id="full-content">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. In fermentum et sollicitudin ac orci phasellus egestas. Facilisi cras fermentum odio eu
feugiat pretium nibh ipsum consequat. Praesent semper feugiat nibh sed pulvinar proin gravida hendrerit
lectus. Porta nibh venenatis cras sed felis eget. Enim sed faucibus turpis in. Non blandit massa enim nec
dui nunc mattis. Ut eu sem integer vitae justo. Lacus vestibulum sed arcu non. Vivamus arcu felis bibendum
ut. Sagittis vitae et leo duis ut diam quam nulla porttitor. Amet est placerat in egestas erat imperdiet.
Dapibus ultrices in iaculis nunc sed augue. Risus sed vulputate odio ut enim blandit volutpat maecenas. Orci
dapibus ultrices in iaculis nunc sed augue lacus. Quam elementum pulvinar etiam non quam. Tempor commodo
ullamcorper a lacus vestibulum sed arcu. Nunc non blandit massa enim nec. Venenatis a condimentum vitae
sapien. Sodales ut eu sem integer vitae justo eget magna. In aliquam sem fringilla ut morbi tincidunt augue.
Diam volutpat commodo sed egestas egestas fringilla phasellus faucibus scelerisque. Semper eget duis at
tellus. Diam donec adipiscing tristique risus nec feugiat in fermentum posuere. Amet volutpat consequat
mauris nunc congue nisi vitae. Hendrerit gravida rutrum quisque non tellus. Aliquet eget sit amet tellus.
Libero id faucibus nisl tincidunt. Amet nulla facilisi morbi tempus iaculis urna id.
</span>
) : (
<span>Loading...</span>
)}
</Box>
</div>
);
};

const AutoSizeUpdatePosition = () => {
const styles = useStyles();
const [overflowBoundary, setOverflowBoundary] = React.useState<HTMLDivElement | null>(null);
const positioningRef = React.useRef<PositioningImperativeRef>(null);
const { containerRef, targetRef } = usePositioning({
position: 'below',
align: 'start',
autoSize: true,
overflowBoundary,
positioningRef,
});

const [isLoaded, setLoaded] = React.useState(false);
const onLoaded = () => setLoaded(true);

React.useEffect(() => {
if (isLoaded) {
positioningRef.current?.updatePosition();
}
}, [isLoaded]);

return (
<div
ref={setOverflowBoundary}
className={styles.boundary}
style={{
display: 'flex',
flexDirection: 'column',
height: 200,
width: 250,
position: 'relative',
}}
>
<button ref={targetRef} style={{ width: 'fit-content', marginLeft: 100, marginTop: 10 }}>
Target
</button>
<Box ref={containerRef} style={{ overflow: 'auto', border: '3px solid green' }}>
{isLoaded ? (
<div id="full-content" style={{ backgroundColor: 'cornflowerblue', width: 200, height: 100 }} />
) : (
<button id="load-content" onClick={onLoaded}>
load content
</button>
)}
</Box>
</div>
);
};

const DisableTether = () => {
const styles = useStyles();
const { containerRef, targetRef } = usePositioning({
Expand Down Expand Up @@ -1019,6 +1123,23 @@ storiesOf('Positioning', module)
.addStory('horizontal overflow', () => <HorizontalOverflow />, { includeRtl: true })
.addStory('pinned', () => <Pinned />)
.addStory('auto size', () => <AutoSize />)
.addStory('auto size with async content', () => (
<StoryWright steps={new Steps().wait('#full-content').snapshot('floating element is within the boundary').end()}>
<AutoSizeAsyncContent />
</StoryWright>
))
.addStory('auto size with async content reset styles on updatePosition', () => (
<StoryWright
steps={new Steps()
.click('#load-content')
.wait('#full-content')
.wait(250) // let updatePosition finish
.snapshot('floating element is fully visible')
.end()}
>
<AutoSizeUpdatePosition />
</StoryWright>
))
.addStory('disable tether', () => <DisableTether />)
.addStory('position fixed', () => <PositionAndAlignProps positionFixed />, { includeRtl: true })
.addStory('virtual element', () => <VirtualElement />)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: simplify autoSize options to make 'always'/'height-always'/'width-always' equivalent to true/'height'/'width'.",
"packageName": "@fluentui/react-positioning",
"email": "yuanboxue@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,100 @@ export interface MaxSizeMiddlewareOptions extends Pick<PositioningOptions, 'over
container: HTMLElement | null;
}

/**
* AutoSizes contains many options from historic implementation.
* Now options 'always'/'height-always'/'width-always' are obsolete.
* This function maps them to true/'height'/'width'
*/
const normalizeAutoSize = (
autoSize?: PositioningOptions['autoSize'],
): { applyMaxWidth: boolean; applyMaxHeight: boolean } => {
switch (autoSize) {
case 'always':
case true:
return {
applyMaxWidth: true,
applyMaxHeight: true,
};

case 'width-always':
case 'width':
return {
applyMaxWidth: true,
applyMaxHeight: false,
};

case 'height-always':
case 'height':
return {
applyMaxWidth: false,
applyMaxHeight: true,
};

default:
return {
applyMaxWidth: false,
applyMaxHeight: false,
};
}
};

export const resetMaxSize = (autoSize: PositioningOptions['autoSize']): Middleware => ({
name: 'resetMaxSize',
fn({ middlewareData, elements }) {
if (middlewareData.maxSizeReset) {
return {};
}

const { applyMaxWidth, applyMaxHeight } = normalizeAutoSize(autoSize);
if (applyMaxWidth) {
elements.floating.style.removeProperty('box-sizing');
elements.floating.style.removeProperty('max-width');
elements.floating.style.removeProperty('width');
}
if (applyMaxHeight) {
elements.floating.style.removeProperty('box-sizing');
elements.floating.style.removeProperty('max-height');
elements.floating.style.removeProperty('height');
}

return {
data: { maxSizeReset: true },
reset: { rects: true },
};
},
});

export function maxSize(autoSize: PositioningOptions['autoSize'], options: MaxSizeMiddlewareOptions): Middleware {
const { container, overflowBoundary } = options;
return size({
...(overflowBoundary && { altBoundary: true, boundary: getBoundary(container, overflowBoundary) }),
apply({ availableHeight, availableWidth, elements, rects }) {
if (autoSize) {
elements.floating.style.setProperty('box-sizing', 'border-box');
}
const { applyMaxWidth, applyMaxHeight } = normalizeAutoSize(autoSize);

const applyMaxWidth = autoSize === 'always' || autoSize === 'width-always';
const widthOverflow = rects.floating.width > availableWidth && (autoSize === true || autoSize === 'width');
const widthOverflow = rects.floating.width > availableWidth;
const heightOverflow = rects.floating.height > availableHeight;

const applyMaxHeight = autoSize === 'always' || autoSize === 'height-always';
const heightOverflow = rects.floating.height > availableHeight && (autoSize === true || autoSize === 'height');

if (applyMaxHeight || heightOverflow) {
elements.floating.style.setProperty('max-height', `${availableHeight}px`);
}
if (heightOverflow) {
elements.floating.style.setProperty('height', `${availableHeight}px`);
elements.floating.style.setProperty('overflow-y', 'auto');
}

if (applyMaxWidth || widthOverflow) {
if (applyMaxWidth) {
elements.floating.style.setProperty('box-sizing', 'border-box');
elements.floating.style.setProperty('max-width', `${availableWidth}px`);
if (widthOverflow) {
elements.floating.style.setProperty('width', `${availableWidth}px`);
if (!elements.floating.style.overflowX) {
elements.floating.style.setProperty('overflow-x', 'auto');
}
}
}
if (widthOverflow) {
elements.floating.style.setProperty('width', `${availableWidth}px`);
elements.floating.style.setProperty('overflow-x', 'auto');

if (applyMaxHeight) {
elements.floating.style.setProperty('box-sizing', 'border-box');
elements.floating.style.setProperty('max-height', `${availableHeight}px`);
if (heightOverflow) {
elements.floating.style.setProperty('height', `${availableHeight}px`);
if (!elements.floating.style.overflowY) {
elements.floating.style.setProperty('overflow-y', 'auto');
}
}
}
},
});
Expand Down
10 changes: 5 additions & 5 deletions packages/react-components/react-positioning/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,11 @@ export interface PositioningOptions {
arrowPadding?: number;

/**
* Applies max-height and max-width on the positioned element to fit it within the available space in viewport.
* true enables this for both width and height when overflow happens.
* 'always' applies `max-height`/`max-width` regardless of overflow.
* 'height' applies `max-height` when overflow happens, and 'width' for `max-width`
* `height-always` applies `max-height` regardless of overflow, and 'width-always' for always applying `max-width`
* Applies styles on the positioned element to fit it within the available space in viewport.
* - true: set styles for max height/width.
* - 'height': set styles for max height.
* - 'width'': set styles for max width.
* Note that options 'always'/'height-always'/'width-always' are now obsolete, and equivalent to true/'height'/'width'.
*/
autoSize?: AutoSize;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
flip as flipMiddleware,
coverTarget as coverTargetMiddleware,
maxSize as maxSizeMiddleware,
resetMaxSize as resetMaxSizeMiddleware,
offset as offsetMiddleware,
intersecting as intersectingMiddleware,
} from './middleware';
Expand Down Expand Up @@ -179,6 +180,7 @@ function usePositioningOptions(options: PositioningOptions) {
const hasScrollableElement = hasScrollParent(container);

const middleware = [
autoSize && resetMaxSizeMiddleware(autoSize),
offset && offsetMiddleware(offset),
coverTarget && coverTargetMiddleware(),
!pinned && flipMiddleware({ container, flipBoundary, hasScrollableElement, isRtl, fallbackPositions }),
Expand Down