Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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(
<MessageBar>
<MessageBarBody>
<MessageBarTitle>Title</MessageBarTitle>
This message bar body is long enough that it needs to reflow to multiple lines.
</MessageBarBody>
</MessageBar>,
);

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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResizeObserver | null>(null);
const prevInlineSizeRef = React.useRef(-1);
const singleLineWidthRef = React.useRef(Number.POSITIVE_INFINITY);

const handleResize: ResizeObserverCallback = React.useCallback(
entries => {
Expand Down Expand Up @@ -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();
Expand Down
Loading