fix(ui): scope ResizeObserver to hook to prevent detached DOM retention (#7345)

This commit is contained in:
白熱
2026-07-27 17:37:07 +08:00
committed by GitHub
parent 098bea0f42
commit 55aac7e027
5 changed files with 50 additions and 126 deletions
@@ -1,82 +0,0 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
type ResizeObserverTestCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;
describe('helper/resize-observer', () => {
beforeEach(() => {
vi.resetModules();
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('should use a shared ResizeObserver and dispatch callbacks', async () => {
const observe = vi.fn();
const unobserve = vi.fn();
let ctorCalls = 0;
const observerCallbackRef: { current?: ResizeObserverTestCallback } = {};
class MockResizeObserver {
constructor(callback: ResizeObserverCallback) {
ctorCalls += 1;
observerCallbackRef.current = callback;
}
observe = observe;
unobserve = unobserve;
}
vi.stubGlobal('ResizeObserver', MockResizeObserver as unknown as typeof ResizeObserver);
const { resizeObserverCtor } = await import('../resize-observer');
const cb1 = vi.fn();
const cb2 = vi.fn();
const targetA = document.createElement('div');
const targetB = document.createElement('div');
const observerA = resizeObserverCtor(cb1);
const observerB = resizeObserverCtor(cb2);
observerA.observe(targetA);
observerB.observe(targetB);
expect(ctorCalls).toBe(1);
expect(observe).toHaveBeenCalledWith(targetA, undefined);
expect(observe).toHaveBeenCalledWith(targetB, undefined);
const entries = [{ target: targetA }] as unknown as ResizeObserverEntry[];
const observerCallback = observerCallbackRef.current;
if (!observerCallback) {
throw new Error('ResizeObserver callback should be initialized');
}
observerCallback(entries, {} as ResizeObserver);
expect(cb1).toHaveBeenCalledTimes(1);
expect(cb2).toHaveBeenCalledTimes(1);
observerA.unobserve(targetA);
observerCallback(entries, {} as ResizeObserver);
expect(cb1).toHaveBeenCalledTimes(1);
expect(cb2).toHaveBeenCalledTimes(2);
expect(unobserve).toHaveBeenCalledWith(targetA);
});
});
@@ -1,40 +0,0 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* All elements are observed by a single ResizeObserver is got greater performance than each element observed by separate ResizeObserver
* See issue https://github.com/WICG/resize-observer/issues/59#issuecomment-408098151
*/
const _resizeObserverCallbacks: Set<ResizeObserverCallback> = new Set();
let _resizeObserver: ResizeObserver;
export function resizeObserverCtor(callback: ResizeObserverCallback) {
if (!_resizeObserver) {
_resizeObserver = new ResizeObserver((...args) => {
_resizeObserverCallbacks.forEach((callback) => callback(...args));
});
}
return {
observe(target: Element, options?: ResizeObserverOptions | undefined) {
_resizeObserverCallbacks.add(callback);
_resizeObserver.observe(target, options);
},
unobserve(target: Element) {
_resizeObserverCallbacks.delete(callback);
_resizeObserver.unobserve(target);
},
};
}
-1
View File
@@ -130,5 +130,4 @@ export {
export { clsx } from './helper/clsx';
export { isBrowser } from './helper/is-browser';
export { render, unmount } from './helper/react-dom';
export { resizeObserverCtor } from './helper/resize-observer';
export { cva } from 'class-variance-authority';
@@ -0,0 +1,47 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderHook } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useScrollYOverContainer } from '../layout';
describe('useScrollYOverContainer', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('should disconnect the ResizeObserver when unmounted', () => {
const disconnect = vi.fn();
class MockResizeObserver {
disconnect = disconnect;
observe = vi.fn();
takeRecords = vi.fn(() => []);
unobserve = vi.fn();
}
vi.stubGlobal('ResizeObserver', MockResizeObserver as unknown as typeof ResizeObserver);
const element = document.createElement('div');
const container = document.createElement('div');
const { unmount } = renderHook(() => useScrollYOverContainer(element, container));
unmount();
expect(disconnect).toHaveBeenCalledTimes(1);
});
});
+3 -3
View File
@@ -16,7 +16,7 @@
import type { Nullable } from '@univerjs/core';
import { IConfigService } from '@univerjs/core';
import { isBrowser, resizeObserverCtor } from '@univerjs/design';
import { isBrowser } from '@univerjs/design';
import { useEffect, useMemo } from 'react';
import { useDependency, useObservable } from '../../utils/di';
import { useEvent } from './event';
@@ -65,10 +65,10 @@ export function useScrollYOverContainer(element: Nullable<HTMLElement>, containe
updater();
const resizeObserver = resizeObserverCtor(updater);
const resizeObserver = new ResizeObserver(updater);
resizeObserver.observe(element);
return () => {
resizeObserver.unobserve(element);
resizeObserver.disconnect();
};
}, [element, container]);
}