Skip to content

Commit

Permalink
Merge branch 'users/srmukher/legendsMultiSelectVBC' of https://github…
Browse files Browse the repository at this point in the history
….com/microsoft/fluentui into users/srmukher/legendsMultiSelectVBC
  • Loading branch information
srmukher committed Dec 19, 2024
2 parents d0158ea + 9df11b9 commit ba28aee
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: MessageBar auto reflow should handle document reflow with `min-content`",
"packageName": "@fluentui/react-message-bar",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ describe('MessageBar', () => {
// do nothing
}
};

// @ts-expect-error https://github.com/jsdom/jsdom/issues/2032
global.IntersectionObserver = class IntersectionObserver {
public observe() {
// do nothing
}
public disconnect() {
// do nothing
}
};
});

beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,91 +1,92 @@
import * as React from 'react';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { isHTMLElement } from '@fluentui/react-utilities';
import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities';

export function useMessageBarReflow(enabled: boolean = false) {
const { targetDocument } = useFluent();
const forceUpdate = React.useReducer(() => ({}), {})[1];
const reflowingRef = React.useRef(false);
// TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286

const resizeObserverRef = React.useRef<ResizeObserver | null>(null);
const prevInlineSizeRef = React.useRef(-1);
const messageBarRef = React.useRef<HTMLElement | null>(null);

const handleResize: ResizeObserverCallback = React.useCallback(
entries => {
// Resize observer is only owned by this component - one resize observer entry expected
// No need to support multiple fragments - one border box entry expected
if (process.env.NODE_ENV !== 'production' && entries.length > 1) {
// eslint-disable-next-line no-console
console.error(
[
'useMessageBarReflow: Resize observer should only have one entry. ',
'If multiple entries are observed, the first entry will be used.',
'This is a bug, please report it to the Fluent UI team.',
].join(' '),
);
}
const [reflowing, setReflowing] = React.useState(false);

const entry = entries[0];
// `borderBoxSize` is not supported before Chrome 84, Firefox 92, nor Safari 15.4
const inlineSize = entry?.borderBoxSize?.[0]?.inlineSize ?? entry?.target.getBoundingClientRect().width;
// This layout effect 'sanity checks' what observers have done
// since DOM has not been flushed when observers run
useIsomorphicLayoutEffect(() => {
if (!messageBarRef.current) {
return;
}

if (inlineSize === undefined || !entry) {
return;
setReflowing(prevReflowing => {
if (!prevReflowing && messageBarRef.current && isReflowing(messageBarRef.current)) {
return true;
}

const { target } = entry;
return prevReflowing;
});
}, [reflowing]);

if (!isHTMLElement(target)) {
return;
}
const handleResize: ResizeObserverCallback = React.useCallback(() => {
if (!messageBarRef.current) {
return;
}

let nextReflowing: boolean | undefined;

// No easy way to really determine when the single line layout will fit
// Just keep try to set single line layout as long as the size is growing
// Will cause flickering when size is being adjusted gradually (i.e. drag) - but this should not be a common case
if (reflowingRef.current) {
if (prevInlineSizeRef.current < inlineSize) {
nextReflowing = false;
}
} else {
const scrollWidth = target.scrollWidth;
if (inlineSize < scrollWidth) {
nextReflowing = true;
}
}
const inlineSize = messageBarRef.current.getBoundingClientRect().width;
const scrollWidth = messageBarRef.current.scrollWidth;

prevInlineSizeRef.current = inlineSize;
if (typeof nextReflowing !== 'undefined' && reflowingRef.current !== nextReflowing) {
reflowingRef.current = nextReflowing;
forceUpdate();
}
},
[forceUpdate],
);
const expanding = prevInlineSizeRef.current < inlineSize;
const overflowing = inlineSize < scrollWidth;

setReflowing(!expanding || overflowing);
}, []);

const handleIntersection: IntersectionObserverCallback = React.useCallback(entries => {
if (entries[0].intersectionRatio < 1) {
setReflowing(true);
}
}, []);

const ref = React.useMemo(() => {
let resizeObserver: ResizeObserver | null = null;
let intersectionObserer: IntersectionObserver | null = null;

const ref = React.useCallback(
(el: HTMLElement | null) => {
return (el: HTMLElement | null) => {
if (!enabled || !el || !targetDocument?.defaultView) {
resizeObserver?.disconnect();
intersectionObserer?.disconnect();
return;
}

resizeObserverRef.current?.disconnect();
messageBarRef.current = el;

const win = targetDocument.defaultView;
const resizeObserver = new win.ResizeObserver(handleResize);
resizeObserverRef.current = resizeObserver;
resizeObserver.observe(el, { box: 'border-box' });
},
[targetDocument, handleResize, enabled],
);
resizeObserver = new win.ResizeObserver(handleResize);
intersectionObserer = new win.IntersectionObserver(handleIntersection, { threshold: 1 });

React.useEffect(() => {
return () => {
resizeObserverRef.current?.disconnect();
intersectionObserer.observe(el);
resizeObserver.observe(el, { box: 'border-box' });
};
}, []);
}, [handleResize, handleIntersection, enabled, targetDocument]);

return { ref, reflowing: reflowingRef.current };
return { ref, reflowing };
}

const isReflowing = (el: HTMLElement) => {
return el.scrollWidth > el.offsetWidth || !isFullyInViewport(el);
};

const isFullyInViewport = (el: HTMLElement) => {
const rect = el.getBoundingClientRect();
const doc = el.ownerDocument;
const win = doc.defaultView;

if (!win) {
return true;
}

return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (win.innerHeight || doc.documentElement.clientHeight) &&
rect.right <= (win.innerWidth || doc.documentElement.clientWidth)
);
};

0 comments on commit ba28aee

Please sign in to comment.