Skip to content

Commit 6f54548

Browse files
author
e.mukhametkhanov
committed
fix(useResizeObserver): fix review comments
1 parent d1596a8 commit 6f54548

2 files changed

Lines changed: 126 additions & 89 deletions

File tree

packages/vkui/src/hooks/useResizeObserver/useResizeObserver.test.tsx

Lines changed: 86 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,21 @@ function createEntry(
1616
height = 50,
1717
borderBoxSize,
1818
contentBoxSize,
19+
devicePixelContentBoxSize,
1920
}: {
2021
width?: number;
2122
height?: number;
2223
borderBoxSize?: Array<{ inlineSize: number; blockSize: number }>;
2324
contentBoxSize?: Array<{ inlineSize: number; blockSize: number }>;
25+
devicePixelContentBoxSize?: Array<{ inlineSize: number; blockSize: number }>;
2426
} = {},
2527
): MockResizeObserverEntry {
2628
return {
2729
target,
2830
contentRect: { width, height } as unknown as DOMRectReadOnly,
2931
borderBoxSize: borderBoxSize as ResizeObserverSize[],
3032
contentBoxSize: contentBoxSize as ResizeObserverSize[],
33+
devicePixelContentBoxSize: devicePixelContentBoxSize as ResizeObserverSize[],
3134
};
3235
}
3336

@@ -51,7 +54,8 @@ describe('useResizeObserver', () => {
5154
const { useResizeObserver } = await import('./useResizeObserver');
5255

5356
const Fixture = () => {
54-
const ref = useResizeObserver<HTMLDivElement>({ rafBatch: false, onResize });
57+
const ref = React.useRef<HTMLDivElement | null>(null);
58+
useResizeObserver<HTMLDivElement>({ rafBatch: false, onResize, ref });
5559
return <div data-testid="target" ref={ref} />;
5660
};
5761

@@ -87,7 +91,8 @@ describe('useResizeObserver', () => {
8791
globalThis.cancelAnimationFrame = vi.fn();
8892

8993
const Fixture = () => {
90-
const ref = useResizeObserver<HTMLDivElement>({ onResize });
94+
const ref = React.useRef<HTMLDivElement | null>(null);
95+
useResizeObserver<HTMLDivElement>({ onResize, ref });
9196
return <div data-testid="target" ref={ref} />;
9297
};
9398

@@ -122,7 +127,8 @@ describe('useResizeObserver', () => {
122127
globalThis.cancelAnimationFrame = vi.fn();
123128

124129
const Fixture = () => {
125-
const ref = useResizeObserver<HTMLDivElement>({ onResize });
130+
const ref = React.useRef<HTMLDivElement | null>(null);
131+
useResizeObserver<HTMLDivElement>({ onResize, ref });
126132
return <div data-testid="target" ref={ref} />;
127133
};
128134

@@ -142,7 +148,8 @@ describe('useResizeObserver', () => {
142148
const { useResizeObserver } = await import('./useResizeObserver');
143149

144150
const Fixture = () => {
145-
const ref = useResizeObserver<HTMLDivElement>({ enabled: false, onResize });
151+
const ref = React.useRef<HTMLDivElement | null>(null);
152+
useResizeObserver<HTMLDivElement>({ enabled: false, onResize, ref });
146153
return <div data-testid="target" ref={ref} />;
147154
};
148155

@@ -158,49 +165,105 @@ describe('useResizeObserver', () => {
158165
expect(onResize).not.toHaveBeenCalled();
159166
});
160167

161-
it('reads size from borderBox/contentBox before contentRect', async () => {
162-
const onResize = vi.fn();
168+
it('reads size according to selected box and falls back to contentRect', async () => {
169+
const onResizeBorder = vi.fn();
170+
const onResizeContent = vi.fn();
171+
const onResizeDevicePixel = vi.fn();
163172
const { useResizeObserver } = await import('./useResizeObserver');
164173

165-
const Fixture = () => {
166-
const ref = useResizeObserver<HTMLDivElement>({ rafBatch: false, onResize });
167-
return <div data-testid="target" ref={ref} />;
174+
const Fixture = ({
175+
box,
176+
onResize,
177+
testId,
178+
}: {
179+
box: ResizeObserverBoxOptions;
180+
onResize: () => void;
181+
testId: string;
182+
}) => {
183+
const ref = React.useRef<HTMLDivElement | null>(null);
184+
useResizeObserver<HTMLDivElement>({ box, rafBatch: false, onResize, ref });
185+
return <div data-testid={testId} ref={ref} />;
168186
};
169187

170-
const { getByTestId } = render(<Fixture />);
171-
const target = getByTestId('target');
172-
const observer = getObserverForTarget(target);
188+
const { getByTestId } = render(
189+
<>
190+
<Fixture box="border-box" onResize={onResizeBorder} testId="border-target" />
191+
<Fixture box="content-box" onResize={onResizeContent} testId="content-target" />
192+
<Fixture
193+
box="device-pixel-content-box"
194+
onResize={onResizeDevicePixel}
195+
testId="device-pixel-target"
196+
/>
197+
</>,
198+
);
199+
200+
const borderTarget = getByTestId('border-target');
201+
const contentTarget = getByTestId('content-target');
202+
const devicePixelTarget = getByTestId('device-pixel-target');
203+
const borderObserver = getObserverForTarget(borderTarget);
204+
const contentObserver = getObserverForTarget(contentTarget);
205+
const devicePixelObserver = getObserverForTarget(devicePixelTarget);
173206

174-
observer.emit([
175-
createEntry(target, {
207+
borderObserver.emit([
208+
createEntry(borderTarget, {
176209
width: 10,
177210
height: 20,
178211
borderBoxSize: [{ inlineSize: 300, blockSize: 150 }],
212+
contentBoxSize: [{ inlineSize: 301, blockSize: 151 }],
179213
}),
180214
]);
181215

182-
expect(onResize).toHaveBeenLastCalledWith(
216+
expect(onResizeBorder).toHaveBeenLastCalledWith(
183217
expect.objectContaining({
184218
width: 300,
185219
height: 150,
186220
}),
187221
);
188222

189-
observer.emit([
190-
createEntry(target, {
223+
contentObserver.emit([
224+
createEntry(contentTarget, {
191225
width: 11,
192226
height: 21,
193-
borderBoxSize: [],
194227
contentBoxSize: [{ inlineSize: 400, blockSize: 220 }],
228+
borderBoxSize: [{ inlineSize: 401, blockSize: 221 }],
195229
}),
196230
]);
197231

198-
expect(onResize).toHaveBeenLastCalledWith(
232+
expect(onResizeContent).toHaveBeenLastCalledWith(
199233
expect.objectContaining({
200234
width: 400,
201235
height: 220,
202236
}),
203237
);
238+
239+
devicePixelObserver.emit([
240+
createEntry(devicePixelTarget, {
241+
width: 12,
242+
height: 22,
243+
devicePixelContentBoxSize: [{ inlineSize: 500, blockSize: 260 }],
244+
}),
245+
]);
246+
247+
expect(onResizeDevicePixel).toHaveBeenLastCalledWith(
248+
expect.objectContaining({
249+
width: 500,
250+
height: 260,
251+
}),
252+
);
253+
254+
devicePixelObserver.emit([
255+
createEntry(devicePixelTarget, {
256+
width: 13,
257+
height: 23,
258+
}),
259+
]);
260+
261+
expect(onResizeDevicePixel).toHaveBeenLastCalledWith(
262+
expect.objectContaining({
263+
width: 13,
264+
height: 23,
265+
}),
266+
);
204267
});
205268

206269
it('creates separate observers for different box options', async () => {
@@ -215,7 +278,8 @@ describe('useResizeObserver', () => {
215278
box: ResizeObserverBoxOptions;
216279
onResize: () => void;
217280
}) => {
218-
const ref = useResizeObserver<HTMLDivElement>({ box, rafBatch: false, onResize });
281+
const ref = React.useRef<HTMLDivElement | null>(null);
282+
useResizeObserver<HTMLDivElement>({ box, rafBatch: false, onResize, ref });
219283
return <div ref={ref} />;
220284
};
221285

@@ -238,7 +302,8 @@ describe('useResizeObserver', () => {
238302
const { useResizeObserver } = await import('./useResizeObserver');
239303

240304
const Fixture = ({ testId, onResize }: { testId: string; onResize: () => void }) => {
241-
const ref = useResizeObserver<HTMLDivElement>({ rafBatch: false, onResize });
305+
const ref = React.useRef<HTMLDivElement | null>(null);
306+
useResizeObserver<HTMLDivElement>({ rafBatch: false, onResize, ref });
242307
return <div data-testid={testId} ref={ref} />;
243308
};
244309

@@ -257,31 +322,4 @@ describe('useResizeObserver', () => {
257322
expect(firstObserver).toBe(secondObserver);
258323
expect(firstObserver.observe).toHaveBeenCalledTimes(2);
259324
});
260-
261-
it('supports external ref passed to hook', async () => {
262-
const onResize = vi.fn();
263-
const { useResizeObserver } = await import('./useResizeObserver');
264-
265-
const Fixture = () => {
266-
const externalRef = React.useRef<HTMLDivElement>(null);
267-
useResizeObserver<HTMLDivElement>({ ref: externalRef, rafBatch: false, onResize });
268-
return <div data-testid="target" ref={externalRef} />;
269-
};
270-
271-
const { getByTestId } = render(<Fixture />);
272-
const target = getByTestId('target');
273-
const observer = getObserverForTarget(target);
274-
275-
expect(observer.observe).toHaveBeenCalledWith(target, { box: 'content-box' });
276-
277-
observer.emit([createEntry(target, { width: 410, height: 210 })]);
278-
279-
expect(onResize).toHaveBeenCalledWith(
280-
expect.objectContaining({
281-
target,
282-
width: 410,
283-
height: 210,
284-
}),
285-
);
286-
});
287325
});

packages/vkui/src/hooks/useResizeObserver/useResizeObserver.ts

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,33 @@ function getResizePool(box: ResizeObserverBoxOptions = 'content-box'): ResizePoo
4545
return pool;
4646
}
4747

48-
function getEntrySize(entry: ResizeObserverEntry) {
49-
const borderBoxSize = entry.borderBoxSize;
50-
51-
if (borderBoxSize?.length) {
52-
return {
53-
width: borderBoxSize[0].inlineSize,
54-
height: borderBoxSize[0].blockSize,
55-
};
56-
}
57-
58-
const contentBoxSize = entry.contentBoxSize;
59-
60-
if (contentBoxSize?.length) {
61-
return {
62-
width: contentBoxSize[0].inlineSize,
63-
height: contentBoxSize[0].blockSize,
64-
};
48+
function getEntrySize(entry: ResizeObserverEntry, box: ResizeObserverBoxOptions) {
49+
switch (box) {
50+
case 'border-box':
51+
if (entry.borderBoxSize?.length) {
52+
return {
53+
width: entry.borderBoxSize[0].inlineSize,
54+
height: entry.borderBoxSize[0].blockSize,
55+
};
56+
}
57+
break;
58+
case 'device-pixel-content-box':
59+
if (entry.devicePixelContentBoxSize?.length) {
60+
return {
61+
width: entry.devicePixelContentBoxSize[0].inlineSize,
62+
height: entry.devicePixelContentBoxSize[0].blockSize,
63+
};
64+
}
65+
break;
66+
case 'content-box':
67+
default:
68+
if (entry.contentBoxSize?.length) {
69+
return {
70+
width: entry.contentBoxSize[0].inlineSize,
71+
height: entry.contentBoxSize[0].blockSize,
72+
};
73+
}
74+
break;
6575
}
6676

6777
return {
@@ -72,7 +82,7 @@ function getEntrySize(entry: ResizeObserverEntry) {
7282

7383
export function useResizeObserver<T extends HTMLElement = HTMLElement>(
7484
options: ElementResizeOptions<T>,
75-
): React.RefCallback<T> {
85+
) {
7686
const {
7787
ref: externalRef,
7888
enabled = true,
@@ -81,28 +91,21 @@ export function useResizeObserver<T extends HTMLElement = HTMLElement>(
8191
onResize: onResizeProp,
8292
} = options;
8393

84-
const [node, setNode] = React.useState<T | null>(null);
85-
8694
const onResize = useStableCallback<[ResizePayload<T>], void>(onResizeProp);
87-
const rafIdRef = React.useRef<number | null>(null);
8895
const latestEntryRef = React.useRef<ResizeObserverEntry | null>(null);
8996

9097
React.useEffect(() => {
91-
const nextNode = externalRef?.current;
92-
if (nextNode && nextNode !== node) {
93-
setNode(nextNode);
94-
}
95-
}, [externalRef, node]);
96-
97-
React.useEffect(() => {
98-
if (!node || !enabled) {
98+
if (!externalRef || !externalRef.current || !enabled) {
9999
return;
100100
}
101+
const node = externalRef.current;
101102

102103
const pool = getResizePool(box);
103104

105+
let rafId: number | null = null;
106+
104107
const emit = (entry: ResizeObserverEntry) => {
105-
const { width, height } = getEntrySize(entry);
108+
const { width, height } = getEntrySize(entry, box);
106109
onResize({
107110
target: node,
108111
width,
@@ -119,12 +122,12 @@ export function useResizeObserver<T extends HTMLElement = HTMLElement>(
119122

120123
latestEntryRef.current = entry;
121124

122-
if (rafIdRef.current !== null) {
125+
if (rafId !== null) {
123126
return;
124127
}
125128

126-
rafIdRef.current = requestAnimationFrame(() => {
127-
rafIdRef.current = null;
129+
rafId = requestAnimationFrame(() => {
130+
rafId = null;
128131
const latest = latestEntryRef.current;
129132
if (latest) {
130133
emit(latest);
@@ -136,18 +139,14 @@ export function useResizeObserver<T extends HTMLElement = HTMLElement>(
136139
pool.observer.observe(node, { box });
137140

138141
return () => {
139-
if (rafIdRef.current !== null) {
140-
cancelAnimationFrame(rafIdRef.current);
141-
rafIdRef.current = null;
142+
if (rafId !== null) {
143+
cancelAnimationFrame(rafId);
144+
rafId = null;
142145
}
143146

144147
latestEntryRef.current = null;
145148
pool.handlers.delete(node);
146149
pool.observer.unobserve(node);
147150
};
148-
}, [node, enabled, box, rafBatch, onResize]);
149-
150-
return React.useCallback<React.RefCallback<T>>((el: T | null) => {
151-
setNode(el);
152-
}, []);
151+
}, [externalRef, enabled, box, rafBatch, onResize]);
153152
}

0 commit comments

Comments
 (0)