diff --git a/change/@fluentui-react-message-bar-92344a29-0bc4-4716-a66c-be75a48e33b1.json b/change/@fluentui-react-message-bar-92344a29-0bc4-4716-a66c-be75a48e33b1.json new file mode 100644 index 00000000000000..d111f8d0ba8ed0 --- /dev/null +++ b/change/@fluentui-react-message-bar-92344a29-0bc4-4716-a66c-be75a48e33b1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix(MessageBar): v9 MessageBar Resize Flicker Fix", + "packageName": "@fluentui/react-message-bar", + "email": "jiangemma@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.test.tsx b/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.test.tsx index 1fac6cac4f3815..389a4cdc6de0ff 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.test.tsx +++ b/packages/react-components/react-message-bar/library/src/components/MessageBar/MessageBar.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { render, act } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { MessageBar } from './MessageBar'; import { AnnounceProvider } from '@fluentui/react-shared-contexts'; @@ -8,6 +8,7 @@ import { MessageBarTitle } from '../MessageBarTitle/MessageBarTitle'; import { MessageBarActions } from '../MessageBarActions/MessageBarActions'; import { resetIdsForTests } from '@fluentui/react-utilities'; import type { MessageBarProps } from './MessageBar.types'; +import { messageBarClassNames } from './useMessageBarStyles.styles'; describe('MessageBar', () => { beforeAll(() => { @@ -92,4 +93,118 @@ describe('MessageBar', () => { expect(announce).toHaveBeenCalledTimes(1); expect(announce).toHaveBeenCalledWith('TitleBody,Action 1Action 2', expect.anything()); }); + + describe('should not flicker during reflow', () => { + let originalResizeObserver: typeof ResizeObserver; + let resizeCallback: ResizeObserverCallback | undefined; + let observedElement: HTMLElement | undefined; + + beforeAll(() => { + originalResizeObserver = global.ResizeObserver; + global.ResizeObserver = class MockResizeObserver { + constructor(cb: ResizeObserverCallback) { + resizeCallback = cb; + } + public observe(el: Element) { + observedElement = el as HTMLElement; + Object.defineProperty(observedElement, 'scrollWidth', { + configurable: true, + get() { + return 640; + }, + }); + } + public unobserve() { + /* noop */ + } + public disconnect() { + /* noop */ + } + } as unknown as typeof ResizeObserver; + }); + + afterAll(() => { + global.ResizeObserver = originalResizeObserver; + }); + + beforeEach(() => { + resizeCallback = undefined; + observedElement = undefined; + }); + + const renderMessageBar = () => + render( + + + Title + This message bar body is long enough that it needs to reflow to multiple lines. + + , + ); + + const isReflowing = (container: HTMLElement) => + container.querySelector(`.${messageBarClassNames.bottomReflowSpacer}`) !== null; + + // Simulate the container being resized (e.g. dragging the window / page) to a given width. + const resizeTo = (inlineSize: number) => + act(() => { + resizeCallback?.( + [ + { + target: observedElement, + borderBoxSize: [{ inlineSize, blockSize: 0 }], + } as unknown as ResizeObserverEntry, + ], + {} as ResizeObserver, + ); + }); + + it('reflows to multiline when the container is narrower than the content', () => { + const { container } = renderMessageBar(); + const singleLineWidth = observedElement!.scrollWidth; + + // Wider than the content - stays single line. + resizeTo(singleLineWidth + 100); + expect(isReflowing(container)).toBe(false); + + // Narrower than the content - reflows to multiline. + resizeTo(singleLineWidth - 100); + expect(isReflowing(container)).toBe(true); + }); + + it('does not flicker while the container width changes during a drag resize', () => { + const { container } = renderMessageBar(); + const singleLineWidth = observedElement!.scrollWidth; + + // Narrow enough to reflow. + resizeTo(singleLineWidth - 100); + expect(isReflowing(container)).toBe(true); + + // Simulate a drag where the width changes but always stays below the width of the single line layout. + const observed: boolean[] = []; + for (const width of [singleLineWidth - 120, singleLineWidth - 40, singleLineWidth - 80, singleLineWidth - 10]) { + resizeTo(width); + observed.push(isReflowing(container)); + } + + // It must stay multiline the entire time - no toggling back to single line. + expect(observed).toEqual([true, true, true, true]); + }); + + it('returns to single line only once there is room for the content again', () => { + const { container } = renderMessageBar(); + const singleLineWidth = observedElement!.scrollWidth; + + resizeTo(singleLineWidth - 100); + expect(isReflowing(container)).toBe(true); + + // Growing but still not enough room - stays reflowed. + resizeTo(singleLineWidth - 1); + expect(isReflowing(container)).toBe(true); + + // Enough room for the single line layout again - returns to single line. + resizeTo(singleLineWidth); + expect(isReflowing(container)).toBe(false); + }); + }); }); diff --git a/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBarReflow.ts b/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBarReflow.ts index b8272479c1ccc2..17f9e7a880ab28 100644 --- a/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBarReflow.ts +++ b/packages/react-components/react-message-bar/library/src/components/MessageBar/useMessageBarReflow.ts @@ -14,7 +14,7 @@ export function useMessageBarReflow(enabled: boolean = false): { // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 const resizeObserverRef = React.useRef(null); - const prevInlineSizeRef = React.useRef(-1); + const singleLineWidthRef = React.useRef(Number.POSITIVE_INFINITY); const handleResize: ResizeObserverCallback = React.useCallback( entries => { @@ -47,21 +47,18 @@ export function useMessageBarReflow(enabled: boolean = false): { 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) { + if (inlineSize >= singleLineWidthRef.current) { nextReflowing = false; } } else { const scrollWidth = target.scrollWidth; if (inlineSize < scrollWidth) { nextReflowing = true; + singleLineWidthRef.current = scrollWidth; } } - prevInlineSizeRef.current = inlineSize; if (typeof nextReflowing !== 'undefined' && reflowingRef.current !== nextReflowing) { reflowingRef.current = nextReflowing; forceUpdate();