@websolutespa/bom-mixer-hooks 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.mjs +1 -1
  4. package/package.json +7 -5
  5. package/src/index.ts +60 -0
  6. package/src/useApi/useApi.test.tsx +193 -0
  7. package/src/useApi/useApi.ts +58 -0
  8. package/src/useBodyScroll/useBodyScroll.test.ts +112 -0
  9. package/src/useBodyScroll/useBodyScroll.ts +89 -0
  10. package/src/useBreakpoint/useBreakpoint.test.tsx +45 -0
  11. package/src/useBreakpoint/useBreakpoint.ts +44 -0
  12. package/src/useCart/useCart.test.tsx +89 -0
  13. package/src/useCart/useCart.tsx +132 -0
  14. package/src/useCheckout/useCheckout.test.tsx +53 -0
  15. package/src/useCheckout/useCheckout.tsx +95 -0
  16. package/src/useClickAnyWhere/useClickAnyWhere.test.ts +15 -0
  17. package/src/useClickAnyWhere/useClickAnyWhere.ts +9 -0
  18. package/src/useClickOut/useClickOut.test.tsx +78 -0
  19. package/src/useClickOut/useClickOut.ts +25 -0
  20. package/src/useCurrency/useCurrency.test.tsx +33 -0
  21. package/src/useCurrency/useCurrency.ts +42 -0
  22. package/src/useCurrentState/useCurrentState.test.tsx +56 -0
  23. package/src/useCurrentState/useCurrentState.ts +22 -0
  24. package/src/useDateTimeFormat/useDateTimeFormat.test.tsx +47 -0
  25. package/src/useDateTimeFormat/useDateTimeFormat.ts +22 -0
  26. package/src/useDebounce/useDebounce.test.ts +49 -0
  27. package/src/useDebounce/useDebounce.ts +15 -0
  28. package/src/useDebounceCallback/useDebounceCallback.test.ts +34 -0
  29. package/src/useDebounceCallback/useDebounceCallback.ts +102 -0
  30. package/src/useDebugChangedProps/useDebugChangedProps.test.tsx +21 -0
  31. package/src/useDebugChangedProps/useDebugChangedProps.ts +17 -0
  32. package/src/useDomObserver/useDomObserver.test.ts +22 -0
  33. package/src/useDomObserver/useDomObserver.ts +26 -0
  34. package/src/useDrawer/useDrawer.test.ts +17 -0
  35. package/src/useDrawer/useDrawer.ts +16 -0
  36. package/src/useElementSize/useElementSize.test.ts +108 -0
  37. package/src/useElementSize/useElementSize.ts +34 -0
  38. package/src/useEventListener/useEventListener.test.ts +128 -0
  39. package/src/useEventListener/useEventListener.ts +43 -0
  40. package/src/useFilters/filter.service.ts +86 -0
  41. package/src/useFilters/filter.ts +152 -0
  42. package/src/useFilters/useFilters.test.ts +48 -0
  43. package/src/useFilters/useFilters.ts +44 -0
  44. package/src/useGenericContext/useGenericContext.test.tsx +19 -0
  45. package/src/useGenericContext/useGenericContext.ts +18 -0
  46. package/src/useGeolocation/geolocation.service.ts +66 -0
  47. package/src/useGeolocation/useGeolocation.test.ts +29 -0
  48. package/src/useGeolocation/useGeolocation.ts +56 -0
  49. package/src/useHttp/useHttp.test.tsx +192 -0
  50. package/src/useHttp/useHttp.ts +52 -0
  51. package/src/useIdle/useIdle.test.ts +31 -0
  52. package/src/useIdle/useIdle.ts +79 -0
  53. package/src/useInfiniteLoader/useInfiniteLoader.test.ts +41 -0
  54. package/src/useInfiniteLoader/useInfiniteLoader.ts +46 -0
  55. package/src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts +3 -0
  56. package/src/useKeyboard/codes.ts +94 -0
  57. package/src/useKeyboard/helper.ts +31 -0
  58. package/src/useKeyboard/useKeyboard.test.tsx +163 -0
  59. package/src/useKeyboard/useKeyboard.ts +91 -0
  60. package/src/useLabel/useLabel.test.tsx +68 -0
  61. package/src/useLabel/useLabel.tsx +43 -0
  62. package/src/useLayout/useLayout.test.tsx +32 -0
  63. package/src/useLayout/useLayout.tsx +19 -0
  64. package/src/useModal/useModal.test.ts +19 -0
  65. package/src/useModal/useModal.ts +16 -0
  66. package/src/useMounted/useMounted.test.tsx +34 -0
  67. package/src/useMounted/useMounted.ts +25 -0
  68. package/src/useNumber/useNumber.test.tsx +36 -0
  69. package/src/useNumber/useNumber.ts +15 -0
  70. package/src/usePage/usePage.test.tsx +31 -0
  71. package/src/usePage/usePage.tsx +19 -0
  72. package/src/usePagination/pagination.service.ts +23 -0
  73. package/src/usePagination/usePagination.test.ts +64 -0
  74. package/src/usePagination/usePagination.ts +47 -0
  75. package/src/usePortal/usePortal.test.ts +24 -0
  76. package/src/usePortal/usePortal.ts +34 -0
  77. package/src/usePrevious/usePrevious.test.ts +21 -0
  78. package/src/usePrevious/usePrevious.ts +9 -0
  79. package/src/useRealSize/useRealSize.ts +41 -0
  80. package/src/useResize/useResize.test.ts +31 -0
  81. package/src/useResize/useResize.ts +18 -0
  82. package/src/useSSR/useSSR.test.tsx +16 -0
  83. package/src/useSSR/useSSR.ts +23 -0
  84. package/src/useScroll/useScroll.test.tsx +25 -0
  85. package/src/useScroll/useScroll.ts +30 -0
  86. package/src/useScrollTo/useScrollTo.test.tsx +105 -0
  87. package/src/useScrollTo/useScrollTo.ts +41 -0
  88. package/src/useSearchParams/useSearchParams.test.ts +59 -0
  89. package/src/useSearchParams/useSearchParams.ts +123 -0
  90. package/src/useSearchParamsEncoded/useSearchParamsEncoded.test.ts +58 -0
  91. package/src/useSearchParamsEncoded/useSearchParamsEncoded.ts +202 -0
  92. package/src/useSorting/useSorting.test.ts +40 -0
  93. package/src/useSorting/useSorting.ts +99 -0
  94. package/src/useSwiperIndex/useSwiperIndex.ts +30 -0
  95. package/src/useSyncedRef/useSyncedRef.test.ts +16 -0
  96. package/src/useSyncedRef/useSyncedRef.ts +18 -0
  97. package/src/useUI/useUI.test.ts +16 -0
  98. package/src/useUI/useUI.ts +44 -0
  99. package/src/useUnit/useUnit.test.tsx +33 -0
  100. package/src/useUnit/useUnit.ts +5 -0
  101. package/src/useUnmountEffect/useUnmountEffect.test.ts +15 -0
  102. package/src/useUnmountEffect/useUnmountEffect.ts +16 -0
  103. package/src/useUser/useUser.test.ts +22 -0
  104. package/src/useUser/useUser.ts +20 -0
  105. package/src/useWindowSize/useWindowSize.test.ts +48 -0
  106. package/src/useWindowSize/useWindowSize.ts +31 -0
@@ -0,0 +1,47 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { LayoutDefaults } from '@websolutespa/bom-mixer-mock';
3
+ import { LayoutProvider } from '../useLayout/useLayout';
4
+ import { useDateTimeFormat } from './useDateTimeFormat';
5
+
6
+ const wrapper = (props: { children: React.ReactNode }) => (
7
+ <LayoutProvider layout={LayoutDefaults}>{props.children}</LayoutProvider>
8
+ );
9
+
10
+ describe('useDateTimeFormat', () => {
11
+ beforeEach(() => {
12
+ jest.spyOn(console, 'error').mockImplementation(() => { });
13
+ });
14
+
15
+ test('should throw without provider', () => {
16
+ expect(() => {
17
+ renderHook(() => useDateTimeFormat());
18
+ }).toThrow('useGenericContext must be used within a Provider');
19
+ });
20
+
21
+ test('should not throw with provider', () => {
22
+ expect(() => {
23
+ renderHook(() => useDateTimeFormat(), { wrapper });
24
+ }).not.toThrow();
25
+ });
26
+
27
+ test('should return formatted dateTime', () => {
28
+ const { result } = renderHook(() => useDateTimeFormat({
29
+ year: 'numeric',
30
+ month: 'short',
31
+ day: 'numeric'
32
+ }), { wrapper });
33
+ const formatted = result.current(new Date(2023, 0, 19));
34
+ expect(formatted).toEqual('Jan 19, 2023');
35
+ });
36
+
37
+ test('should return formatted dateTime with date string', () => {
38
+ const { result } = renderHook(() => useDateTimeFormat({
39
+ year: 'numeric',
40
+ month: 'short',
41
+ day: 'numeric'
42
+ }), { wrapper });
43
+ const formatted = result.current('2023/1/19');
44
+ expect(formatted).toEqual('Jan 19, 2023');
45
+ });
46
+
47
+ });
@@ -0,0 +1,22 @@
1
+ import { useLayout } from '../useLayout/useLayout';
2
+
3
+ export function useDateTimeFormat(
4
+ options: Intl.DateTimeFormatOptions = {
5
+ // weekday: 'long',
6
+ year: 'numeric',
7
+ month: 'numeric',
8
+ day: 'numeric'
9
+ },
10
+ locale?: string
11
+ ): (value: Date | string) => string {
12
+
13
+ const layout = useLayout();
14
+
15
+ const dateTimeFormat = (value: Date | string): string => {
16
+ const date = value instanceof Date ? value : new Date(value);
17
+ const formattedValue = new Intl.DateTimeFormat(locale || layout.locale, options).format(date);
18
+ return formattedValue;
19
+ };
20
+
21
+ return dateTimeFormat;
22
+ }
@@ -0,0 +1,49 @@
1
+ // readme https://testing-library.com/
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useDebounce } from './useDebounce';
4
+
5
+ describe('useDebounce', () => {
6
+ afterEach(() => {
7
+ jest.useRealTimers();
8
+ });
9
+
10
+ test('should return debounce value', () => {
11
+ const value = 'value';
12
+ const { result } = renderHook(() => useDebounce(value));
13
+ expect(value).toBe(result.current);
14
+ });
15
+
16
+ test('should debounce with default debounce 500 ms', () => {
17
+ mockSetTimeout();
18
+ const value = 'value';
19
+ renderHook(() => useDebounce(value));
20
+ expect(setTimeout).toHaveBeenCalledTimes(1);
21
+ expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 500);
22
+ });
23
+
24
+ test('should debounce with given debounce', () => {
25
+ mockSetTimeout();
26
+ const value = 'value';
27
+ renderHook(() => useDebounce(value, 1337));
28
+ expect(setTimeout).toHaveBeenCalledTimes(1);
29
+ expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1337);
30
+ });
31
+
32
+ test('should call clearTimeout on unmount', () => {
33
+ mockClearTimeout();
34
+ const value = 'value';
35
+ const { unmount } = renderHook(() => useDebounce(value));
36
+ unmount();
37
+ expect(clearTimeout).toHaveBeenCalledTimes(1);
38
+ });
39
+ });
40
+
41
+ function mockSetTimeout() {
42
+ jest.useFakeTimers();
43
+ jest.spyOn(global, 'setTimeout');
44
+ }
45
+
46
+ function mockClearTimeout() {
47
+ jest.useFakeTimers();
48
+ jest.spyOn(global, 'clearTimeout');
49
+ }
@@ -0,0 +1,15 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export function useDebounce<T>(value: T, msec: number = 500): T {
4
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
5
+
6
+ useEffect(() => {
7
+ const timer = setTimeout(() => setDebouncedValue(value), msec);
8
+
9
+ return () => {
10
+ clearTimeout(timer);
11
+ };
12
+ }, [value, msec]);
13
+
14
+ return debouncedValue;
15
+ }
@@ -0,0 +1,34 @@
1
+ // readme https://testing-library.com/
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { useDebounceCallback } from './useDebounceCallback';
4
+
5
+ const renderDebounceCallback = (callback: () => unknown, msec: number = 500) =>
6
+ renderHook(() => useDebounceCallback(callback, msec));
7
+
8
+ describe('useDebounceCallback', () => {
9
+ jest.useFakeTimers();
10
+
11
+ it('should wait 100ms before invoking the callback', () => {
12
+ const callback = jest.fn();
13
+ const { result } = renderDebounceCallback(callback, 100);
14
+ const debouncedCallback = result.current;
15
+
16
+ act(debouncedCallback);
17
+ act(debouncedCallback);
18
+ act(debouncedCallback);
19
+
20
+ act(() => jest.advanceTimersByTime(50));
21
+ expect(callback).toBeCalledTimes(0);
22
+ act(() => jest.advanceTimersByTime(100));
23
+ expect(callback).toBeCalledTimes(1);
24
+
25
+ act(debouncedCallback);
26
+ act(debouncedCallback);
27
+ act(debouncedCallback);
28
+
29
+ act(() => jest.advanceTimersByTime(99));
30
+ expect(callback).toHaveBeenCalledTimes(1);
31
+ act(() => jest.advanceTimersByTime(1));
32
+ expect(callback).toHaveBeenCalledTimes(2);
33
+ });
34
+ });
@@ -0,0 +1,102 @@
1
+ import { useCallback, useRef } from 'react';
2
+
3
+ /**
4
+ * Returns a memoized function that will only call the passed function when it hasn't been called for the msec period
5
+ * @param func The function to be called
6
+ * @param msec Wait period after function hasn't been called for
7
+ * @returns A memoized function that is debounced
8
+ */
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ export function useDebounceCallback<T extends (...args: any[]) => any>(callback: T, msec: number = 500) {
11
+ // Use a ref to store the timeout between renders
12
+ // and prevent changes to it from causing re-renders
13
+ const timeout = useRef<ReturnType<typeof setTimeout>>();
14
+
15
+ return useCallback((...args: unknown[]) => {
16
+ const later = () => {
17
+ clearTimeout(timeout.current);
18
+ callback(...args);
19
+ };
20
+
21
+ clearTimeout(timeout.current);
22
+ timeout.current = setTimeout(later, msec);
23
+ }, [callback, msec]);
24
+
25
+ }
26
+
27
+ /* eslint-disable @typescript-eslint/no-explicit-any */
28
+ /*
29
+ import { DependencyList, useRef } from 'react';
30
+ import { useUnmountEffect } from '../useUnmountEffect/useUnmountEffect';
31
+
32
+ export interface DebouncedFunction<Fn extends (...args: any[]) => any> {
33
+ (this: ThisParameterType<Fn>, ...args: Parameters<Fn>): void;
34
+ }
35
+ */
36
+
37
+ /**
38
+ * Makes passed function debounced, otherwise acts like `useCallback`.
39
+ *
40
+ * @param callback Function that will be debounced.
41
+ * @param deps Dependencies list when to update callback.
42
+ * @param delay Debounce delay.
43
+ * @param maxWait The maximum time `callback` is allowed to be delayed before
44
+ * it's invoked. 0 means no max wait.
45
+ */
46
+ /*
47
+ export function useDebouncedCallback<Fn extends (...args: any[]) => any>(
48
+ callback: Fn,
49
+ deps: DependencyList,
50
+ delay: number,
51
+ maxWait = 0
52
+ ): DebouncedFunction<Fn> {
53
+ const timeout = useRef<ReturnType<typeof setTimeout>>();
54
+ const waitTimeout = useRef<ReturnType<typeof setTimeout>>();
55
+ const lastCall = useRef<{ args: Parameters<Fn>; this: ThisParameterType<Fn> }>();
56
+ const clear = () => {
57
+ if (timeout.current) {
58
+ clearTimeout(timeout.current);
59
+ timeout.current = undefined;
60
+ }
61
+ if (waitTimeout.current) {
62
+ clearTimeout(waitTimeout.current);
63
+ waitTimeout.current = undefined;
64
+ }
65
+ };
66
+ // cancel scheduled execution on unmount
67
+ useUnmountEffect(clear);
68
+ return useMemo(
69
+ () => {
70
+ const execute = () => {
71
+ // barely possible to test this line
72
+ // istanbul ignore next
73
+ if (!lastCall.current) return;
74
+ const context = lastCall.current;
75
+ lastCall.current = undefined;
76
+ callback.apply(context.this, context.args);
77
+ clear();
78
+ };
79
+ // eslint-disable-next-line func-names
80
+ const wrapped = function (this, ...args) {
81
+ if (timeout.current) {
82
+ clearTimeout(timeout.current);
83
+ }
84
+ lastCall.current = { args, this: this };
85
+ // plan regular execution
86
+ timeout.current = setTimeout(execute, delay);
87
+ // plan maxWait execution if required
88
+ if (maxWait > 0 && !waitTimeout.current) {
89
+ waitTimeout.current = setTimeout(execute, maxWait);
90
+ }
91
+ } as DebouncedFunction<Fn>;
92
+ Object.defineProperties(wrapped, {
93
+ length: { value: callback.length },
94
+ name: { value: `${callback.name || 'anonymous'}__debounced__${delay}` },
95
+ });
96
+ return wrapped;
97
+ },
98
+ // eslint-disable-next-line react-hooks/exhaustive-deps,@typescript-eslint/no-unsafe-assignment
99
+ [delay, maxWait, ...deps]
100
+ );
101
+ }
102
+ */
@@ -0,0 +1,21 @@
1
+
2
+
3
+ import { renderHook } from '@testing-library/react';
4
+ import { useDebugChangedProps } from './useDebugChangedProps';
5
+
6
+ describe('useDebugChangedProps', () => {
7
+ /*
8
+ beforeEach(() => {
9
+ jest.spyOn(console, 'log').mockImplementation(() => { });
10
+ });
11
+ */
12
+
13
+ it('should work correctly', () => {
14
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
15
+ const { rerender } = renderHook((props) => useDebugChangedProps(props), { initialProps: { test: 1 } });
16
+ expect(logSpy).toHaveBeenCalledTimes(0);
17
+ rerender({ test: 2 });
18
+ expect(logSpy).toHaveBeenCalledTimes(1);
19
+ expect(logSpy).toHaveBeenCalledWith('Changed props:', { test: [1, 2] });
20
+ });
21
+ });
@@ -0,0 +1,17 @@
1
+ import { ComponentProps, useEffect, useRef } from 'react';
2
+
3
+ export function useDebugChangedProps(props: ComponentProps<any>): void {
4
+ const ref = useRef(props);
5
+ useEffect(() => {
6
+ const changedProps = Object.entries(props).reduce((object: { [key: string]: unknown }, [key, value]) => {
7
+ if (ref.current[key] !== value) {
8
+ object[key] = [ref.current[key], value];
9
+ }
10
+ return object;
11
+ }, {});
12
+ if (Object.keys(changedProps).length > 0) {
13
+ console.log('Changed props:', changedProps);
14
+ }
15
+ ref.current = props;
16
+ });
17
+ }
@@ -0,0 +1,22 @@
1
+
2
+
3
+ // readme https://testing-library.com/
4
+ import { renderHook, screen, waitFor } from '@testing-library/react';
5
+ import { useDomObserver } from './useDomObserver';
6
+
7
+ describe('useDomObserver', () => {
8
+
9
+ it('should work correctly', async () => {
10
+ const handler = jest.fn();
11
+ const div = document.createElement('div');
12
+ div.setAttribute('data-testid', 'test');
13
+ document.body.appendChild(div);
14
+ const ref = { current: div };
15
+ renderHook(() => useDomObserver(ref, handler));
16
+ expect(handler).toHaveBeenCalledTimes(0);
17
+ div.innerText = 'test';
18
+ await waitFor(() => expect(screen.getByTestId('test').innerText).toBe('test'));
19
+ expect(handler).toHaveBeenCalledTimes(0); // !!! MutationObserver not working
20
+ // expect(handler).toHaveBeenCalledWith('Changed props:', { test: [1, 2] });
21
+ });
22
+ });
@@ -0,0 +1,26 @@
1
+ import { MutableRefObject, useEffect } from 'react';
2
+
3
+ export function useDomObserver(ref: MutableRefObject<HTMLElement | null> | undefined, callback: MutationCallback = () => { }) {
4
+
5
+ const config = { attributes: false, childList: true, subtree: true };
6
+
7
+ useEffect(() => {
8
+ if (!ref || !ref.current) {
9
+ return;
10
+ }
11
+ let unmount = false;
12
+ const done: MutationCallback = (...params) => {
13
+ // console.log('useDomObserver.MutationCallback', params);
14
+ if (unmount) {
15
+ return;
16
+ }
17
+ callback(...params);
18
+ };
19
+ const observer = new MutationObserver(done);
20
+ observer.observe(ref.current, config);
21
+ return () => {
22
+ unmount = true;
23
+ observer.disconnect();
24
+ };
25
+ }, [ref]);
26
+ }
@@ -0,0 +1,17 @@
1
+ // readme https://testing-library.com/
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { useDrawer } from './useDrawer';
4
+
5
+ describe('useDrawer', () => {
6
+ it('should work correctly', async () => {
7
+ const { result } = renderHook(() => useDrawer());
8
+ let [drawer, openDrawer, closeDrawer] = result.current;
9
+ expect(drawer).toBe(undefined);
10
+ act(() => openDrawer('test'));
11
+ [drawer, openDrawer, closeDrawer] = result.current;
12
+ expect(drawer).toBe('test');
13
+ act(() => closeDrawer());
14
+ [drawer, openDrawer, closeDrawer] = result.current;
15
+ expect(drawer).toBe(undefined);
16
+ });
17
+ });
@@ -0,0 +1,16 @@
1
+ import { UIValue, useUI } from '../useUI/useUI';
2
+
3
+ export function useDrawer(): [drawer: UIValue, open: (value?: string) => void, close: () => void] {
4
+ const drawer = useUI(state => state.views.drawer);
5
+ const reduce = useUI(state => state.actions.reduce);
6
+ function setDrawer(value?: string | number) {
7
+ reduce('drawer', (views) => value);
8
+ }
9
+ function openDrawer(value?: string | number) {
10
+ return setDrawer(value);
11
+ }
12
+ function closeDrawer() {
13
+ return setDrawer();
14
+ }
15
+ return [drawer, openDrawer, closeDrawer];
16
+ }
@@ -0,0 +1,108 @@
1
+ // readme https://testing-library.com/
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { useElementSize } from './useElementSize';
4
+
5
+ const setupHook = () => renderHook(() => useElementSize());
6
+ const originalOffsetHeight = Object.getOwnPropertyDescriptor(
7
+ HTMLElement.prototype,
8
+ 'offsetHeight',
9
+ ) as PropertyDescriptor;
10
+ const originalOffsetWidth = Object.getOwnPropertyDescriptor(
11
+ HTMLElement.prototype,
12
+ 'offsetWidth',
13
+ ) as PropertyDescriptor;
14
+ const resizeElement = (
15
+ node: HTMLElement,
16
+ dimension: 'width' | 'height',
17
+ value: number,
18
+ ): void => {
19
+ if (dimension === 'height') {
20
+ return void Object.defineProperty(node, 'offsetHeight', {
21
+ configurable: true,
22
+ value,
23
+ });
24
+ }
25
+
26
+ if (dimension === 'width') {
27
+ void Object.defineProperty(node, 'offsetWidth', {
28
+ configurable: true,
29
+ value,
30
+ });
31
+ }
32
+ };
33
+ const dom = document.createElement('div');
34
+
35
+ describe('useElementSize', () => {
36
+ afterAll(() => {
37
+ Object.defineProperty(
38
+ HTMLElement.prototype,
39
+ 'offsetHeight',
40
+ originalOffsetHeight,
41
+ );
42
+ Object.defineProperty(
43
+ HTMLElement.prototype,
44
+ 'offsetWidth',
45
+ originalOffsetWidth,
46
+ );
47
+ });
48
+
49
+ it('should initialize', () => {
50
+ const { result } = setupHook();
51
+ const [setRef, size] = result.current;
52
+ expect(typeof size.height).toBe('number');
53
+ expect(typeof size.width).toBe('number');
54
+ expect(setRef).toBeInstanceOf(Function);
55
+ });
56
+
57
+ it('should match the corresponding height', () => {
58
+ const height_1 = 360;
59
+ const height_2 = 600;
60
+ const height_3 = 1024;
61
+ const { result, rerender } = setupHook();
62
+ const [setRef] = result.current;
63
+ /* 1 */
64
+ act(() => {
65
+ setRef(dom);
66
+ resizeElement(dom, 'height', height_1);
67
+ });
68
+ expect(result.current[1].height).toEqual(height_1);
69
+ /* 2 */
70
+ act(() => {
71
+ resizeElement(dom, 'height', height_2);
72
+ });
73
+ rerender();
74
+ expect(result.current[1].height).toEqual(height_2);
75
+ /* 3 */
76
+ act(() => {
77
+ resizeElement(dom, 'height', height_3);
78
+ });
79
+ rerender();
80
+ expect(result.current[1].height).toEqual(height_3);
81
+ });
82
+
83
+ it('should match the corresponding width', () => {
84
+ const width_1 = 360;
85
+ const width_2 = 600;
86
+ const width_3 = 1024;
87
+ const { result, rerender } = setupHook();
88
+ const [setRef] = result.current;
89
+ /* 1 */
90
+ act(() => {
91
+ setRef(dom);
92
+ resizeElement(dom, 'width', width_1);
93
+ });
94
+ expect(result.current[1].width).toEqual(width_1);
95
+ /* 2 */
96
+ act(() => {
97
+ resizeElement(dom, 'width', width_2);
98
+ });
99
+ rerender();
100
+ expect(result.current[1].width).toEqual(width_2);
101
+ /* 3 */
102
+ act(() => {
103
+ resizeElement(dom, 'width', width_3);
104
+ });
105
+ rerender();
106
+ expect(result.current[1].width).toEqual(width_3);
107
+ });
108
+ });
@@ -0,0 +1,34 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { useEventListener } from '../useEventListener/useEventListener';
3
+ import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
4
+
5
+ type Size = {
6
+ width: number;
7
+ height: number;
8
+ };
9
+
10
+ export function useElementSize<T extends HTMLElement = HTMLDivElement>(): [(node: T | null) => void, Size] {
11
+ // Mutable values like 'ref.current' aren't valid dependencies
12
+ // because mutating them doesn't re-render the component.
13
+ // Instead, we use a state as a ref to be reactive.
14
+ const [ref, setRef] = useState<T | null>(null);
15
+ const [size, setSize] = useState<Size>({ width: 0, height: 0 });
16
+
17
+ // Prevent too many rendering using useCallback
18
+ const onResize = useCallback(() => {
19
+ setSize({
20
+ width: ref?.offsetWidth || 0,
21
+ height: ref?.offsetHeight || 0,
22
+ });
23
+ // eslint-disable-next-line react-hooks/exhaustive-deps
24
+ }, [ref?.offsetHeight, ref?.offsetWidth]);
25
+
26
+ useEventListener('resize', onResize);
27
+
28
+ useIsomorphicLayoutEffect(() => {
29
+ onResize();
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, [ref?.offsetHeight, ref?.offsetWidth]);
32
+
33
+ return [setRef, size];
34
+ }
@@ -0,0 +1,128 @@
1
+ // readme https://testing-library.com/
2
+ import { fireEvent, renderHook } from '@testing-library/react';
3
+ import { useEventListener } from './useEventListener';
4
+
5
+ declare global {
6
+ interface WindowEventMap {
7
+ 'test-event': CustomEvent
8
+ }
9
+
10
+ interface HTMLElementEventMap {
11
+ 'test-event': CustomEvent
12
+ }
13
+
14
+ interface DocumentEventMap {
15
+ 'test-event': CustomEvent
16
+ }
17
+ }
18
+
19
+ const windowAddEventListenerSpy = jest.spyOn(window, 'addEventListener');
20
+ const windowRemoveEventListenerSpy = jest.spyOn(window, 'removeEventListener');
21
+
22
+ const ref = { current: document.createElement('div') };
23
+ const refAddEventListenerSpy = jest.spyOn(ref.current, 'addEventListener');
24
+ const refRemoveEventListenerSpy = jest.spyOn(ref.current, 'removeEventListener');
25
+ const docRef = { current: window.document };
26
+ const docAddEventListenerSpy = jest.spyOn(docRef.current, 'addEventListener');
27
+ const docRemoveEventListenerSpy = jest.spyOn(
28
+ docRef.current,
29
+ 'removeEventListener',
30
+ );
31
+
32
+ describe('useEventListener', () => {
33
+ afterEach(() => {
34
+ jest.clearAllMocks();
35
+ });
36
+
37
+ it('should bind/unbind the event listener to the window when element is not provided', () => {
38
+ const eventName = 'test-event';
39
+ const handler = jest.fn();
40
+ const options = undefined;
41
+ const { unmount } = renderHook(() => useEventListener(eventName, handler));
42
+ expect(windowAddEventListenerSpy).toHaveBeenCalledWith(
43
+ eventName,
44
+ expect.any(Function),
45
+ options,
46
+ );
47
+ unmount();
48
+ expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith(
49
+ eventName,
50
+ expect.any(Function),
51
+ );
52
+ });
53
+
54
+ it('should bind/unbind the event listener to the element when element is provided', () => {
55
+ const eventName = 'test-event';
56
+ const handler = jest.fn();
57
+ const options = undefined;
58
+ const { unmount } = renderHook(() =>
59
+ useEventListener(eventName, handler, ref, options),
60
+ );
61
+ expect(refAddEventListenerSpy).toHaveBeenCalledTimes(1);
62
+ expect(refAddEventListenerSpy).toHaveBeenCalledWith(
63
+ eventName,
64
+ expect.any(Function),
65
+ options,
66
+ );
67
+ unmount();
68
+ expect(refRemoveEventListenerSpy).toHaveBeenCalledWith(
69
+ eventName,
70
+ expect.any(Function),
71
+ );
72
+ });
73
+
74
+ it('should bind/unbind the event listener to the document when document is provided', () => {
75
+ const eventName = 'test-event';
76
+ const handler = jest.fn();
77
+ const options = undefined;
78
+ const { unmount } = renderHook(() =>
79
+ useEventListener(eventName, handler, docRef, options),
80
+ );
81
+ expect(docAddEventListenerSpy).toHaveBeenCalledTimes(1);
82
+ expect(docAddEventListenerSpy).toHaveBeenCalledWith(
83
+ eventName,
84
+ expect.any(Function),
85
+ options,
86
+ );
87
+ unmount();
88
+ expect(docRemoveEventListenerSpy).toHaveBeenCalledWith(
89
+ eventName,
90
+ expect.any(Function),
91
+ );
92
+ });
93
+
94
+ it('should pass the options to the event listener', () => {
95
+ const eventName = 'test-event';
96
+ const handler = jest.fn();
97
+ const options = {
98
+ passive: true,
99
+ once: true,
100
+ capture: true,
101
+ };
102
+ renderHook(() => useEventListener(eventName, handler, undefined, options));
103
+ expect(windowAddEventListenerSpy).toHaveBeenCalledWith(
104
+ eventName,
105
+ expect.any(Function),
106
+ options,
107
+ );
108
+ });
109
+
110
+ it('should call the event listener handler when the event is triggered', () => {
111
+ const eventName = 'click';
112
+ const handler = jest.fn();
113
+ renderHook(() => useEventListener(eventName, handler, ref));
114
+ fireEvent.click(ref.current);
115
+ expect(handler).toHaveBeenCalledTimes(1);
116
+ });
117
+
118
+ it('should have the correct event type', () => {
119
+ const clickHandler = jest.fn();
120
+ const keydownHandler = jest.fn();
121
+ renderHook(() => useEventListener('click', clickHandler, ref));
122
+ renderHook(() => useEventListener('keydown', keydownHandler, ref));
123
+ fireEvent.click(ref.current);
124
+ fireEvent.keyDown(ref.current);
125
+ expect(clickHandler).toHaveBeenCalledWith(expect.any(MouseEvent));
126
+ expect(keydownHandler).toHaveBeenCalledWith(expect.any(KeyboardEvent));
127
+ });
128
+ });