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
37 changes: 21 additions & 16 deletions src/components/GridLayout/ReactGridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ResizeObserverPolyfill from 'resize-observer-polyfill';
import {DROPPING_ELEMENT_CLASS_NAME, OVERLAY_CLASS_NAME} from '../../constants';

const GRID_LAYOUT_CLASS_NAME = 'react-grid-layout';
const SETTLED_WIDTH_COMMIT_DELAY = 150;

const isRefObject = (
value: React.Ref<HTMLDivElement>,
Expand Down Expand Up @@ -535,7 +536,7 @@ class PassiveWidthProvider extends React.Component<
private gridItems: HTMLElement[] = [];
private lastAppliedWidth?: number;
private resizeObserver?: ResizeObserver;
private settledWidthCommitFrame?: number;
private settledWidthCommitTimer?: number;
private isComponentMounted = false;

componentDidMount() {
Expand All @@ -554,10 +555,7 @@ class PassiveWidthProvider extends React.Component<
this.isComponentMounted = false;
this.resizeObserver?.disconnect();
this.resizeObserver = undefined;
if (this.settledWidthCommitFrame !== undefined) {
window.cancelAnimationFrame(this.settledWidthCommitFrame);
this.settledWidthCommitFrame = undefined;
}
this.cancelSettledWidthCommit();
this.element?.removeEventListener('pointerdown', this.handlePointerDown, true);
this.element = null;
}
Expand Down Expand Up @@ -655,6 +653,7 @@ class PassiveWidthProvider extends React.Component<
return;
}

this.cancelSettledWidthCommit();
flushSync(() => {
this.setState((state) => ({
interactionRevision: state.interactionRevision + 1,
Expand All @@ -664,6 +663,7 @@ class PassiveWidthProvider extends React.Component<
};

private syncSettledWidth = () => {
this.cancelSettledWidthCommit();
if (this.state.settledWidth === this.widthRef.current) {
return;
}
Expand All @@ -673,18 +673,23 @@ class PassiveWidthProvider extends React.Component<
});
};

private scheduleSettledWidthCommit = () => {
if (this.settledWidthCommitFrame !== undefined) {
window.cancelAnimationFrame(this.settledWidthCommitFrame);
private cancelSettledWidthCommit = () => {
if (this.settledWidthCommitTimer === undefined) {
return;
}
this.settledWidthCommitFrame = window.requestAnimationFrame(() => {
this.settledWidthCommitFrame = window.requestAnimationFrame(() => {
this.settledWidthCommitFrame = undefined;
if (this.state.settledWidth !== this.widthRef.current) {
this.setState({settledWidth: this.widthRef.current});
}
});
});
window.clearTimeout(this.settledWidthCommitTimer);
this.settledWidthCommitTimer = undefined;
};

private scheduleSettledWidthCommit = () => {
this.cancelSettledWidthCommit();
// DOM grid items update in ResizeObserver; React reconciles only after resizing settles.
this.settledWidthCommitTimer = window.setTimeout(() => {
this.settledWidthCommitTimer = undefined;
if (this.isComponentMounted && this.state.settledWidth !== this.widthRef.current) {
this.setState({settledWidth: this.widthRef.current});
}
}, SETTLED_WIDTH_COMMIT_DELAY);
};

private syncElement = () => {
Expand Down
41 changes: 15 additions & 26 deletions src/components/GridLayout/__tests__/passive-resize.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ type ResizeObserverCallback = (entries: ResizeObserverEntry[]) => void;

const resizeObservers: TestResizeObserver[] = [];
const mockPolyfillObservers: TestResizeObserver[] = [];
const animationFrameCallbacks = new Map<number, FrameRequestCallback>();
let animationFrameId = 0;

jest.mock('resize-observer-polyfill', () => ({
__esModule: true,
Expand Down Expand Up @@ -86,12 +84,6 @@ const emitPolyfillResizeFor = (element: Element, width: number) => {
});
};

const runAnimationFrame = () => {
const callbacks = Array.from(animationFrameCallbacks.values());
animationFrameCallbacks.clear();
callbacks.forEach((callback) => callback(0));
};

const gridItem = (
<GridItem
adjustWidgetLayout={jest.fn()}
Expand All @@ -106,17 +98,11 @@ describe('Layout passive resize', () => {
beforeEach(() => {
resizeObservers.length = 0;
mockPolyfillObservers.length = 0;
animationFrameCallbacks.clear();
animationFrameId = 0;
global.ResizeObserver = TestResizeObserver as unknown as typeof ResizeObserver;
window.requestAnimationFrame = jest.fn((callback) => {
animationFrameId += 1;
animationFrameCallbacks.set(animationFrameId, callback);
return animationFrameId;
});
window.cancelAnimationFrame = jest.fn((frame) => {
animationFrameCallbacks.delete(frame);
});
});

afterEach(() => {
jest.useRealTimers();
});

test('does not render ReactGridLayout for passive container resize', () => {
Expand Down Expand Up @@ -166,7 +152,8 @@ describe('Layout passive resize', () => {
},
);

test('commits only latest width after delivery between two quiet frames', () => {
test('commits only latest width after resize settles', () => {
jest.useFakeTimers();
const renderSpy = jest.spyOn(ReactGridLayout.prototype, 'render');
render(
<Layout
Expand All @@ -182,17 +169,17 @@ describe('Layout passive resize', () => {

act(() => {
emitResize(900);
runAnimationFrame();
jest.advanceTimersByTime(100);
emitResize(1000);
});

expect(renderSpy).toHaveBeenCalledTimes(rendersBeforeResize);
act(() => {
runAnimationFrame();
jest.advanceTimersByTime(149);
});
expect(renderSpy).toHaveBeenCalledTimes(rendersBeforeResize);
act(() => {
runAnimationFrame();
jest.advanceTimersByTime(1);
});
expect(renderSpy).toHaveBeenCalledTimes(rendersBeforeResize + 1);
expect((renderSpy.mock.instances.at(-1) as unknown as ReactGridLayout).props.width).toBe(
Expand All @@ -201,7 +188,8 @@ describe('Layout passive resize', () => {
renderSpy.mockRestore();
});

test('cancels second quiet frame on unmount', () => {
test('cancels settled width commit on unmount', () => {
jest.useFakeTimers();
const renderSpy = jest.spyOn(ReactGridLayout.prototype, 'render');
const {unmount} = render(
<Layout
Expand All @@ -217,14 +205,15 @@ describe('Layout passive resize', () => {

act(() => {
emitResize(900);
runAnimationFrame();
jest.advanceTimersByTime(100);
});
expect(renderSpy).toHaveBeenCalledTimes(rendersBeforeResize);
expect(jest.getTimerCount()).toBe(1);

unmount();
expect(window.cancelAnimationFrame).toHaveBeenLastCalledWith(2);
expect(jest.getTimerCount()).toBe(0);
act(() => {
runAnimationFrame();
jest.advanceTimersByTime(150);
});
expect(renderSpy).toHaveBeenCalledTimes(rendersBeforeResize);
renderSpy.mockRestore();
Expand Down
Loading