Skip to content

Commit 662f73d

Browse files
committed
233. [PATCH] Improve useElementSize hook
1 parent a3ddcc4 commit 662f73d

6 files changed

Lines changed: 162 additions & 134 deletions

File tree

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { MutableRefObject } from 'react';
2+
13
interface ElementSize {
24
width: number;
35
height: number;
@@ -11,26 +13,24 @@ interface DetectedState extends ElementSize {
1113
status: 'detected';
1214
}
1315

14-
interface UnsupportedState {
15-
status: 'unsupported';
16-
}
17-
18-
type ElementSizeState = UndetectedState | DetectedState | UnsupportedState;
16+
type ElementSizeState = UndetectedState | DetectedState;
1917

2018
type ElementSizeStateStatus = ElementSizeState['status'];
2119

22-
/** Configuration object. */
23-
interface UseElementSizeConfig {
24-
/** It quantifies how much time is needed to broadcast the next event in milliseconds. */
20+
interface ElementSizeConfig {
2521
delay?: number;
2622
}
2723

24+
type ElementSizeReturn<T extends HTMLElement> = Readonly<
25+
[ElementSizeState, MutableRefObject<T | null>]
26+
>;
27+
2828
export type {
2929
UndetectedState,
3030
ElementSize,
3131
DetectedState,
32-
UnsupportedState,
3332
ElementSizeStateStatus,
3433
ElementSizeState,
35-
UseElementSizeConfig,
34+
ElementSizeReturn,
35+
ElementSizeConfig,
3636
};
Lines changed: 108 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,128 @@
11
import { renderHook, waitFor, render, screen } from '@testing-library/react';
22
import { useElementSize } from './use-element-size';
3-
import type { ElementSizeStateStatus, ElementSizeState } from './defs';
43

54
describe('Element size can be detected when: ', () => {
6-
describe('tracks and', () => {
7-
const originalObserver = global.ResizeObserver;
8-
let disconnectSpy: jest.Mock;
9-
let observeSpy: jest.Mock;
10-
11-
const HEIGHT = 600;
12-
const WIDTH = 800;
13-
14-
beforeEach(() => {
15-
disconnectSpy = jest.fn();
16-
observeSpy = jest.fn();
17-
18-
global.ResizeObserver = class MockedResizeObserver {
19-
constructor(cb: ResizeObserverCallback) {
20-
setTimeout(() => {
21-
cb(
22-
[
23-
{
24-
contentRect: {
25-
height: HEIGHT,
26-
width: WIDTH,
27-
},
28-
},
29-
] as ResizeObserverEntry[],
30-
this
31-
);
32-
}, 150);
33-
}
34-
35-
// eslint-disable-next-line @typescript-eslint/no-empty-function
36-
observe = observeSpy;
37-
38-
// eslint-disable-next-line @typescript-eslint/no-empty-function
39-
unobserve = () => {};
5+
const height = 600;
6+
const width = 800;
7+
8+
beforeEach(() => {
9+
global.ResizeObserver = class MockedResizeObserver {
10+
observe = jest.fn();
11+
unobserve = jest.fn();
12+
disconnect = jest.fn();
13+
};
14+
});
4015

41-
// eslint-disable-next-line @typescript-eslint/no-empty-function
42-
disconnect = disconnectSpy;
43-
};
44-
});
16+
const ComponentFixture = () => {
17+
const [state, ref] = useElementSize<HTMLDivElement>();
18+
return (
19+
<div ref={ref}>
20+
{state.status === 'detected'
21+
? `detected: width: ${state.width}, height: ${state.height}`
22+
: state.status}
23+
</div>
24+
);
25+
};
26+
27+
it('calculates size for typical HTML node', () => {
28+
render(<ComponentFixture />);
29+
screen.getByText(`detected: width: 0, height: 0`);
30+
});
4531

46-
afterEach(() => {
47-
global.ResizeObserver = originalObserver;
32+
it('listens for resize of element', async () => {
33+
const observeSpy = jest.fn();
34+
35+
global.ResizeObserver = class MockedResizeObserver {
36+
constructor(cb: ResizeObserverCallback) {
37+
setTimeout(() => {
38+
cb(
39+
[
40+
{
41+
contentRect: {
42+
height,
43+
width,
44+
},
45+
},
46+
] as ResizeObserverEntry[],
47+
this
48+
);
49+
}, 150);
50+
}
51+
52+
observe = observeSpy;
53+
unobserve = jest.fn();
54+
disconnect = jest.fn();
55+
};
56+
57+
render(<ComponentFixture />);
58+
59+
await waitFor(() => {
60+
screen.getByText(`detected: width: ${width}, height: ${height}`);
4861
});
62+
});
4963

50-
it('updates state if listening body', async () => {
51-
const { result } = renderHook(() => useElementSize());
52-
53-
expect(observeSpy).toHaveBeenCalledTimes(1);
54-
expect(observeSpy).toHaveBeenCalledWith(document.body);
55-
expect(result.current.state).toEqual({
56-
status: 'undetected',
57-
} as ElementSizeState);
58-
59-
await waitFor(() => {
60-
expect(result.current.state).toEqual({
61-
status: 'detected',
62-
height: HEIGHT,
63-
width: WIDTH,
64-
} as ElementSizeState);
64+
it('listens for resize for body', async () => {
65+
const observeSpy = jest.fn();
66+
67+
global.ResizeObserver = class MockedResizeObserver {
68+
constructor(cb: ResizeObserverCallback) {
69+
setTimeout(() => {
70+
cb(
71+
[
72+
{
73+
contentRect: {
74+
height,
75+
width,
76+
},
77+
},
78+
] as ResizeObserverEntry[],
79+
this
80+
);
81+
}, 150);
82+
}
83+
84+
observe = observeSpy;
85+
unobserve = jest.fn();
86+
disconnect = jest.fn();
87+
};
88+
89+
const { result } = renderHook(() => useElementSize());
90+
91+
await waitFor(() => {
92+
expect(result.current[0]).toEqual({
93+
status: 'detected',
94+
height,
95+
width,
6596
});
6697
});
98+
});
6799

68-
it('updates state if listening native HTML element', async () => {
69-
const ComponentFixture = () => {
70-
const { ref, state } = useElementSize<HTMLDivElement>();
71-
return (
72-
<div ref={ref}>
73-
{state.status === 'detected'
74-
? `detected: width: ${state.width}, height: ${state.height}`
75-
: state.status}
76-
</div>
77-
);
78-
};
79-
80-
render(<ComponentFixture />);
81-
82-
const element = document.createElement('div');
83-
element.innerHTML = 'undetected' as ElementSizeStateStatus;
100+
it('disconnects after unmount', () => {
101+
const disconnectSpy = jest.fn();
102+
global.ResizeObserver = class MockedResizeObserver {
103+
observe = jest.fn();
104+
unobserve = jest.fn();
105+
disconnect = disconnectSpy;
106+
};
84107

85-
expect(observeSpy).toHaveBeenCalledTimes(1);
86-
expect(observeSpy).toHaveBeenCalledWith(element);
108+
const { unmount } = renderHook(() => useElementSize());
87109

88-
screen.getByText('undetected' as ElementSizeStateStatus);
110+
unmount();
89111

90-
await waitFor(() => {
91-
screen.getByText(`detected: width: ${WIDTH}, height: ${HEIGHT}`);
92-
});
93-
});
112+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
113+
});
94114

95-
it('disconnects after unmount', () => {
96-
const { unmount } = renderHook(() => useElementSize());
115+
it('calculates size for body when mounted', () => {
116+
jest
117+
.spyOn(document.body, 'getBoundingClientRect')
118+
.mockReturnValue({ height, width } as DOMRect);
97119

98-
unmount();
120+
const { result } = renderHook(() => useElementSize());
99121

100-
expect(disconnectSpy).toHaveBeenCalledTimes(1);
122+
expect(result.current[0]).toEqual({
123+
status: 'detected',
124+
height,
125+
width,
101126
});
102127
});
103128
});
Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
11
import { useEffect, useRef, useState, useMemo } from 'react';
22
import { Subject, throttleTime } from 'rxjs';
3-
import type { ElementSizeState, UseElementSizeConfig } from './defs';
3+
import type {
4+
ElementSizeState,
5+
ElementSizeConfig,
6+
ElementSizeReturn,
7+
} from './defs';
8+
import { useIsomorphicLayoutEffect } from '../use-isomorphic-layout-effect';
49

510
/**
611
* The hook responsible for detecting the height and width of
712
* any HTML element. By default it checks body.
813
*
914
* It returns reference and state to work with.
10-
* @param {UseElementSizeConfig} config - Configuration object.
15+
* @param {ElementSizeConfig} config - Configuration object.
1116
*/
1217
const useElementSize = <T extends HTMLElement>(
13-
config?: UseElementSizeConfig
14-
) => {
18+
config?: ElementSizeConfig
19+
): ElementSizeReturn<T> => {
1520
const [state, setState] = useState<ElementSizeState>({
1621
status: 'undetected',
1722
});
18-
19-
const ref = useRef<T>(null);
23+
const ref = useRef<T | null>(null);
2024
const observerRef = useRef<ResizeObserver | null>(null);
2125

22-
const changed = useMemo(() => new Subject<ElementSizeState>(), []);
23-
// eslint-disable-next-line react-hooks/exhaustive-deps
24-
const changed$ = useMemo(() => changed.asObservable(), []);
26+
const { changed, changed$ } = useMemo(() => {
27+
const changed = new Subject<ElementSizeState>();
28+
const changed$ = changed.asObservable();
29+
return { changed, changed$ };
30+
}, []);
2531

2632
useEffect(() => {
2733
const sub = changed$.pipe(throttleTime(config?.delay ?? 150)).subscribe({
@@ -33,41 +39,32 @@ const useElementSize = <T extends HTMLElement>(
3339
return () => {
3440
sub.unsubscribe();
3541
};
36-
// eslint-disable-next-line react-hooks/exhaustive-deps
37-
}, []);
42+
}, [changed$, config?.delay]);
3843

39-
useEffect(() => {
44+
useIsomorphicLayoutEffect(() => {
4045
const observeElement = () => {
41-
if (!ref?.current && !document.body) {
42-
changed.next({ status: 'unsupported' });
43-
return;
44-
}
46+
const target = ref?.current ?? document.body;
47+
48+
const { width, height } = target.getBoundingClientRect();
49+
50+
setState({ status: 'detected', height, width });
4551

4652
observerRef.current = new ResizeObserver((entries) => {
4753
const { width, height } = entries[0].contentRect;
48-
49-
changed.next({
50-
status: 'detected',
51-
height,
52-
width,
53-
});
54+
changed.next({ status: 'detected', height, width });
5455
});
5556

56-
observerRef.current.observe(ref?.current ?? document.body);
57+
observerRef.current.observe(target);
5758
};
5859

5960
observeElement();
6061

6162
return () => {
6263
observerRef.current?.disconnect();
6364
};
64-
// eslint-disable-next-line react-hooks/exhaustive-deps
65-
}, []);
65+
}, [changed]);
6666

67-
return {
68-
state,
69-
ref,
70-
};
67+
return [state, ref];
7168
};
7269

7370
export { useElementSize };

system/libs/figa-ui/src/lib/creator-layout/__snapshots__/creator-layout.test.tsx.snap

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
exports[`Creator layout can be used when: [FRAGILE] assigns classes 1`] = `
44
<DocumentFragment>
55
<div
6-
class="sc-jSUZER DFtOs creator-layout undetected my-class"
6+
class="sc-jSUZER DFtOs creator-layout code my-class"
77
>
88
<div
99
class="sc-eDvSVe kyLWFC"
@@ -12,6 +12,20 @@ exports[`Creator layout can be used when: [FRAGILE] assigns classes 1`] = `
1212
Navigation
1313
</button>
1414
</div>
15+
<div
16+
class="sc-bcXHqe CwXFH"
17+
>
18+
<div>
19+
Code content
20+
</div>
21+
</div>
22+
<div
23+
class="sc-dkrFOg fPnWkf"
24+
>
25+
<button>
26+
Code Toolbox Button
27+
</button>
28+
</div>
1529
</div>
1630
</DocumentFragment>
1731
`;

system/libs/figa-ui/src/lib/creator-layout/creator-layout.test.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,10 @@ describe('Creator layout can be used when: ', () => {
7474

7575
mock();
7676

77-
const { container, asFragment } = creatorLayoutFixture({
77+
const { asFragment } = creatorLayoutFixture({
7878
className: 'my-class',
7979
});
8080

81-
const component = container.querySelector('.creator-layout');
82-
83-
expect(component?.className).toContain(
84-
'creator-layout undetected my-class'
85-
);
8681
expect(asFragment()).toMatchSnapshot();
8782

8883
restore();

0 commit comments

Comments
 (0)