Skip to content

Commit 4384dfb

Browse files
Dimitri-WEI-Lingfengweilingfeng
andauthored
fix: should update elements' position's height after resized in a short duration (#106)
Co-authored-by: weilingfeng <weilingfeng@tezgin.com>
1 parent b8eae68 commit 4384dfb

File tree

2 files changed

+129
-21
lines changed

2 files changed

+129
-21
lines changed

src/use-resize-observer.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import "resize-observer-polyfill";
2+
import { elementsCache } from "./elements-cache";
3+
import { createPositioner } from "./use-positioner";
4+
import { createResizeObserver } from "./use-resize-observer";
5+
6+
// mock requestAnimationFrame
7+
// https://stackoverflow.com/questions/61593774/how-do-i-test-code-that-uses-requestanimationframe-in-jest
8+
beforeEach(() => {
9+
jest.useFakeTimers();
10+
11+
let count = 0;
12+
jest
13+
.spyOn(window, "requestAnimationFrame")
14+
.mockImplementation(
15+
(cb: any) => setTimeout(() => cb(100 * ++count), 100) as any as number
16+
);
17+
});
18+
19+
afterEach(() => {
20+
window.requestAnimationFrame.mockRestore();
21+
jest.clearAllTimers();
22+
});
23+
24+
jest.mock("resize-observer-polyfill", () => {
25+
class ResizeObserver {
26+
els = [];
27+
callback: any;
28+
constructor(callback) {
29+
this.callback = callback;
30+
}
31+
observe(el) {
32+
this.els.push(el);
33+
}
34+
unobserve() {
35+
// do nothing
36+
}
37+
disconnect() {}
38+
39+
resize(index: number, height: number) {
40+
this.els[index].offsetHeight = height;
41+
this.callback(
42+
this.els.map((el) => ({
43+
target: el,
44+
}))
45+
);
46+
}
47+
}
48+
window.ResizeObserver = ResizeObserver;
49+
return {
50+
__esModule: true,
51+
default: ResizeObserver,
52+
};
53+
});
54+
55+
describe("createResizeObserver", () => {
56+
it("should update elements' position's height after resized in a short duration", async () => {
57+
const els = [
58+
{ offsetHeight: 100 },
59+
{ offsetHeight: 100 },
60+
{ offsetHeight: 100 },
61+
{ offsetHeight: 100 },
62+
{ offsetHeight: 100 },
63+
];
64+
65+
const positioner = createPositioner(5, 100);
66+
67+
els.forEach((el, i) => {
68+
elementsCache.set(el, i);
69+
positioner.set(i, el.offsetHeight);
70+
});
71+
72+
const observer = createResizeObserver(positioner, () => {});
73+
els.forEach((el) => {
74+
observer.observe(el);
75+
});
76+
77+
for (let i = 0; i < 5; i++) {
78+
observer.resize(i, 200);
79+
}
80+
81+
await jest.runAllTimers();
82+
83+
for (let i = 0; i < 5; i++) {
84+
expect(positioner.get(i).height).toBe(200);
85+
}
86+
});
87+
});

src/use-resize-observer.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export function useResizeObserver(positioner: Positioner) {
2121
return resizeObserver;
2222
}
2323

24+
const _handlerForType = rafSchd((target: HTMLElement) => {});
25+
26+
type IHandler = typeof _handlerForType;
27+
2428
/**
2529
* Creates a resize observer that fires an `updater` callback whenever the height of
2630
* one or many cells change. The `useResizeObserver()` hook is using this under the hood.
@@ -33,33 +37,48 @@ export const createResizeObserver = trieMemoize(
3337
// TODO: figure out a way to test this
3438
/* istanbul ignore next */
3539
(positioner: Positioner, updater: (updates: number[]) => void) => {
36-
const handleEntries = rafSchd(((entries) => {
37-
const updates: number[] = [];
38-
let i = 0;
39-
40-
for (; i < entries.length; i++) {
41-
const entry = entries[i];
42-
const height = (entry.target as HTMLElement).offsetHeight;
43-
44-
if (height > 0) {
45-
const index = elementsCache.get(entry.target);
46-
47-
if (index !== void 0) {
48-
const position = positioner.get(index);
49-
50-
if (position !== void 0 && height !== position.height)
51-
updates.push(index, height);
52-
}
53-
}
54-
}
40+
const updates: number[] = [];
5541

42+
const update = rafSchd(() => {
5643
if (updates.length > 0) {
5744
// Updates the size/positions of the cell with the resize
5845
// observer updates
5946
positioner.update(updates);
6047
updater(updates);
6148
}
62-
}) as ResizeObserverCallback);
49+
updates.length = 0;
50+
});
51+
52+
const commonHandler = (target: HTMLElement) => {
53+
const height = target.offsetHeight;
54+
if (height > 0) {
55+
const index = elementsCache.get(target);
56+
if (index !== void 0) {
57+
const position = positioner.get(index);
58+
if (position !== void 0 && height !== position.height)
59+
updates.push(index, height);
60+
}
61+
}
62+
update();
63+
};
64+
65+
const handlers = new Map<number, IHandler>();
66+
const handleEntries: ResizeObserverCallback = (entries) => {
67+
let i = 0;
68+
69+
for (; i < entries.length; i++) {
70+
const entry = entries[i];
71+
const index = elementsCache.get(entry.target);
72+
73+
if (index === void 0) continue;
74+
let handler = handlers.get(index);
75+
if (!handler) {
76+
handler = rafSchd(commonHandler);
77+
handlers.set(index, handler);
78+
}
79+
handler(entry.target as HTMLElement);
80+
}
81+
};
6382

6483
const ro = new ResizeObserver(handleEntries);
6584
// Overrides the original disconnect to include cancelling handling the entries.
@@ -68,7 +87,9 @@ export const createResizeObserver = trieMemoize(
6887
const disconnect = ro.disconnect.bind(ro);
6988
ro.disconnect = () => {
7089
disconnect();
71-
handleEntries.cancel();
90+
handlers.forEach((handler) => {
91+
handler.cancel();
92+
});
7293
};
7394

7495
return ro;

0 commit comments

Comments
 (0)