Skip to content

Commit 8ca682c

Browse files
tassoevanggazzo
andauthored
feat: New hooks for element size tracking (#413)
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
1 parent 531803a commit 8ca682c

File tree

38 files changed

+1818
-1577
lines changed

38 files changed

+1818
-1577
lines changed

packages/css-in-js/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"@rollup/plugin-typescript": "^8.2.1",
5050
"@types/jest": "^27.0.2",
5151
"@types/stylis": "^4.0.1",
52-
"eslint": "^7.32.0",
52+
"eslint": "^8.2.0",
5353
"jest": "^27.3.1",
5454
"lint-all": "workspace:tools/lint-all",
5555
"lint-staged": "^11.2.6",

packages/css-supports/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"@rocket.chat/eslint-config-alt": "workspace:packages/eslint-config-alt",
4141
"@rocket.chat/prettier-config": "workspace:packages/prettier-config",
4242
"@types/jest": "^27.0.2",
43-
"eslint": "^7.32.0",
43+
"eslint": "^8.2.0",
4444
"jest": "^27.3.1",
4545
"lint-all": "workspace:tools/lint-all",
4646
"lint-staged": "^11.2.6",

packages/emitter/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"@rollup/plugin-node-resolve": "^13.0.0",
4949
"@rollup/plugin-typescript": "^8.2.1",
5050
"@types/jest": "^27.0.2",
51-
"eslint": "^7.32.0",
51+
"eslint": "^8.2.0",
5252
"jest": "^27.3.1",
5353
"lint-all": "workspace:tools/lint-all",
5454
"lint-staged": "^11.2.6",

packages/eslint-config-alt/package.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,21 @@
3737
"prettier": "^2.3.2"
3838
},
3939
"devDependencies": {
40-
"@babel/eslint-parser": "^7.15.8",
41-
"eslint": "^7.32.0",
40+
"@babel/eslint-parser": "^7.16.3",
41+
"eslint": "^8.2.0",
4242
"lint-all": "workspace:tools/lint-all",
4343
"lint-staged": "^11.2.6",
4444
"prettier": "^2.3.2"
4545
},
4646
"dependencies": {
4747
"@rocket.chat/eslint-config": "^0.4.0",
48-
"@typescript-eslint/eslint-plugin": "^4.33.0",
49-
"@typescript-eslint/parser": "^4.33.0",
48+
"@typescript-eslint/eslint-plugin": "^5.3.1",
49+
"@typescript-eslint/parser": "^5.3.1",
5050
"eslint-config-prettier": "^8.3.0",
5151
"eslint-import-resolver-typescript": "^2.5.0",
52-
"eslint-plugin-import": "^2.25.2",
52+
"eslint-plugin-import": "^2.25.3",
5353
"eslint-plugin-prettier": "^4.0.0",
54-
"eslint-plugin-react": "^7.26.1",
55-
"eslint-plugin-react-hooks": "^4.2.0"
54+
"eslint-plugin-react": "^7.27.0",
55+
"eslint-plugin-react-hooks": "^4.3.0"
5656
}
5757
}

packages/fuselage-hooks/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"@types/react-dom": "^17.0.11",
5656
"@types/resize-observer-browser": "^0.1.6",
5757
"@types/use-subscription": "^1.0.0",
58-
"eslint": "^7.32.0",
58+
"eslint": "^8.2.0",
5959
"jest": "^27.3.1",
6060
"lint-all": "workspace:tools/lint-all",
6161
"lint-staged": "^11.2.6",

packages/fuselage-hooks/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export * from './useAutoFocus';
2+
export * from './useBorderBoxSize';
23
export * from './useBreakpoints';
34
export * from './useClipboard';
45
export * from './useDarkMode';
6+
export * from './useContentBoxSize';
57
export * from './useDebouncedCallback';
68
export * from './useDebouncedReducer';
79
export * from './useDebouncedState';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import { renderHook } from '@testing-library/react-hooks/server';
6+
import { useRef } from 'react';
7+
8+
import { useBorderBoxSize } from './useBorderBoxSize';
9+
10+
it('immediately returns zero size', () => {
11+
const { result } = renderHook(() => useBorderBoxSize(useRef()));
12+
13+
expect(result.current.inlineSize).toStrictEqual(0);
14+
expect(result.current.blockSize).toStrictEqual(0);
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { renderHook, act } from '@testing-library/react-hooks';
2+
import { useRef, RefObject } from 'react';
3+
import { withResizeObserverMock } from 'testing-utils/mocks/withResizeObserverMock';
4+
5+
import { useBorderBoxSize } from './useBorderBoxSize';
6+
7+
withResizeObserverMock();
8+
9+
beforeAll(() => {
10+
jest.useFakeTimers();
11+
});
12+
13+
let element: HTMLElement;
14+
15+
beforeEach(() => {
16+
element = document.createElement('div');
17+
element.style.width = '40px';
18+
element.style.height = '30px';
19+
element.style.padding = '5px';
20+
document.body.append(element);
21+
});
22+
23+
afterEach(() => {
24+
element.remove();
25+
});
26+
27+
const wrapRef = (ref: RefObject<HTMLElement>) => {
28+
Object.assign(ref, { current: element });
29+
return ref;
30+
};
31+
32+
it('immediately returns size', async () => {
33+
const { result } = renderHook(() => useBorderBoxSize(wrapRef(useRef())));
34+
35+
expect(result.current.inlineSize).toStrictEqual(50);
36+
expect(result.current.blockSize).toStrictEqual(40);
37+
});
38+
39+
it('gets the observed element size after resize', async () => {
40+
const { result } = renderHook(() => useBorderBoxSize(wrapRef(useRef())));
41+
42+
// triggers MutationObserver
43+
await act(async () => {
44+
element.style.width = '30px';
45+
element.style.height = '40px';
46+
element.style.padding = '15px';
47+
});
48+
49+
// waits for debounced state mutation
50+
await act(async () => {
51+
jest.advanceTimersByTime(0);
52+
});
53+
54+
expect(result.current.inlineSize).toStrictEqual(60);
55+
expect(result.current.blockSize).toStrictEqual(70);
56+
57+
// triggers MutationObserver
58+
await act(async () => {
59+
element.style.boxSizing = 'border-box';
60+
});
61+
62+
// waits for debounced state mutation
63+
await act(async () => {
64+
jest.advanceTimersByTime(0);
65+
});
66+
67+
expect(result.current.inlineSize).toStrictEqual(30);
68+
expect(result.current.blockSize).toStrictEqual(40);
69+
});
70+
71+
it('debounces the observed element size', async () => {
72+
const halfDelay = 50;
73+
const delay = 2 * halfDelay;
74+
75+
const { result } = renderHook(() =>
76+
useBorderBoxSize(wrapRef(useRef()), { debounceDelay: delay })
77+
);
78+
79+
// triggers MutationObserver
80+
await act(async () => {
81+
element.style.width = '30px';
82+
element.style.height = '40px';
83+
element.style.padding = '15px';
84+
});
85+
86+
// waits for debounced state mutation
87+
await act(async () => {
88+
jest.advanceTimersByTime(halfDelay);
89+
});
90+
91+
expect(result.current.inlineSize).toStrictEqual(50);
92+
expect(result.current.blockSize).toStrictEqual(40);
93+
94+
// wait the callback trigger from ResizeObserver
95+
await act(async () => {
96+
jest.advanceTimersByTime(halfDelay);
97+
});
98+
99+
expect(result.current.inlineSize).toStrictEqual(60);
100+
expect(result.current.blockSize).toStrictEqual(70);
101+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { RefObject, useState } from 'react';
2+
3+
import { useDebouncedCallback } from './useDebouncedCallback';
4+
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
5+
6+
export const useBorderBoxSize = (
7+
ref: RefObject<HTMLElement>,
8+
options: {
9+
debounceDelay?: number;
10+
} = {}
11+
): Readonly<{
12+
inlineSize: number;
13+
blockSize: number;
14+
}> => {
15+
const [size, setSize] = useState(() => ({
16+
inlineSize: ref.current?.offsetWidth ?? 0,
17+
blockSize: ref.current?.offsetHeight ?? 0,
18+
}));
19+
20+
const setSizeWithDebounce = useDebouncedCallback(
21+
setSize,
22+
options.debounceDelay
23+
);
24+
25+
useIsomorphicLayoutEffect(() => {
26+
const element = ref.current;
27+
28+
if (!element) {
29+
return;
30+
}
31+
32+
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
33+
if (entries.length === 0 || entries[0].borderBoxSize.length === 0) {
34+
return;
35+
}
36+
37+
const borderBoxSize = entries[0].borderBoxSize[0];
38+
39+
setSizeWithDebounce((prevSize) => {
40+
if (
41+
prevSize.inlineSize === borderBoxSize.inlineSize &&
42+
prevSize.blockSize === borderBoxSize.blockSize
43+
) {
44+
return prevSize;
45+
}
46+
47+
return {
48+
inlineSize: borderBoxSize.inlineSize,
49+
blockSize: borderBoxSize.blockSize,
50+
};
51+
});
52+
});
53+
54+
observer.observe(element);
55+
56+
setSize({
57+
inlineSize: element.offsetWidth,
58+
blockSize: element.offsetHeight,
59+
});
60+
61+
return () => {
62+
observer.unobserve(element);
63+
};
64+
}, [setSizeWithDebounce]);
65+
66+
return size;
67+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import { renderHook } from '@testing-library/react-hooks/server';
6+
import { useRef } from 'react';
7+
8+
import { useContentBoxSize } from './useContentBoxSize';
9+
10+
it('immediately returns zero size', () => {
11+
const { result } = renderHook(() => useContentBoxSize(useRef()));
12+
13+
expect(result.current.inlineSize).toStrictEqual(0);
14+
expect(result.current.blockSize).toStrictEqual(0);
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { renderHook, act } from '@testing-library/react-hooks';
2+
import { useRef, RefObject } from 'react';
3+
import { withResizeObserverMock } from 'testing-utils/mocks/withResizeObserverMock';
4+
5+
import { useContentBoxSize } from './useContentBoxSize';
6+
7+
withResizeObserverMock();
8+
9+
beforeAll(() => {
10+
jest.useFakeTimers();
11+
});
12+
13+
let element: HTMLElement;
14+
15+
beforeEach(() => {
16+
element = document.createElement('div');
17+
element.style.width = '40px';
18+
element.style.height = '30px';
19+
element.style.padding = '5px';
20+
document.body.append(element);
21+
});
22+
23+
afterEach(() => {
24+
element.remove();
25+
});
26+
27+
const wrapRef = (ref: RefObject<HTMLElement>) => {
28+
Object.assign(ref, { current: element });
29+
return ref;
30+
};
31+
32+
it('immediately returns size', async () => {
33+
const { result } = renderHook(() => useContentBoxSize(wrapRef(useRef())));
34+
35+
expect(result.current.inlineSize).toStrictEqual(40);
36+
expect(result.current.blockSize).toStrictEqual(30);
37+
});
38+
39+
it('gets the observed element size after resize', async () => {
40+
const { result } = renderHook(() => useContentBoxSize(wrapRef(useRef())));
41+
42+
// triggers MutationObserver
43+
await act(async () => {
44+
element.style.width = '30px';
45+
element.style.height = '40px';
46+
element.style.padding = '15px';
47+
});
48+
49+
// waits for debounced state mutation
50+
await act(async () => {
51+
jest.advanceTimersByTime(0);
52+
});
53+
54+
expect(result.current.inlineSize).toStrictEqual(30);
55+
expect(result.current.blockSize).toStrictEqual(40);
56+
57+
// triggers MutationObserver
58+
await act(async () => {
59+
element.style.boxSizing = 'border-box';
60+
});
61+
62+
// waits for debounced state mutation
63+
await act(async () => {
64+
jest.advanceTimersByTime(0);
65+
});
66+
67+
expect(result.current.inlineSize).toStrictEqual(0);
68+
expect(result.current.blockSize).toStrictEqual(10);
69+
});
70+
71+
it('debounces the observed element size', async () => {
72+
const halfDelay = 50;
73+
const delay = 2 * halfDelay;
74+
75+
const { result } = renderHook(() =>
76+
useContentBoxSize(wrapRef(useRef()), { debounceDelay: delay })
77+
);
78+
79+
// triggers MutationObserver
80+
await act(async () => {
81+
element.style.width = '30px';
82+
element.style.height = '40px';
83+
element.style.padding = '15px';
84+
});
85+
86+
// waits for debounced state mutation
87+
await act(async () => {
88+
jest.advanceTimersByTime(halfDelay);
89+
});
90+
91+
expect(result.current.inlineSize).toStrictEqual(40);
92+
expect(result.current.blockSize).toStrictEqual(30);
93+
94+
// wait the callback trigger from ResizeObserver
95+
await act(async () => {
96+
jest.advanceTimersByTime(halfDelay);
97+
});
98+
99+
expect(result.current.inlineSize).toStrictEqual(30);
100+
expect(result.current.blockSize).toStrictEqual(40);
101+
});

0 commit comments

Comments
 (0)