@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.
- package/CHANGELOG.md +12 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +7 -5
- package/src/index.ts +60 -0
- package/src/useApi/useApi.test.tsx +193 -0
- package/src/useApi/useApi.ts +58 -0
- package/src/useBodyScroll/useBodyScroll.test.ts +112 -0
- package/src/useBodyScroll/useBodyScroll.ts +89 -0
- package/src/useBreakpoint/useBreakpoint.test.tsx +45 -0
- package/src/useBreakpoint/useBreakpoint.ts +44 -0
- package/src/useCart/useCart.test.tsx +89 -0
- package/src/useCart/useCart.tsx +132 -0
- package/src/useCheckout/useCheckout.test.tsx +53 -0
- package/src/useCheckout/useCheckout.tsx +95 -0
- package/src/useClickAnyWhere/useClickAnyWhere.test.ts +15 -0
- package/src/useClickAnyWhere/useClickAnyWhere.ts +9 -0
- package/src/useClickOut/useClickOut.test.tsx +78 -0
- package/src/useClickOut/useClickOut.ts +25 -0
- package/src/useCurrency/useCurrency.test.tsx +33 -0
- package/src/useCurrency/useCurrency.ts +42 -0
- package/src/useCurrentState/useCurrentState.test.tsx +56 -0
- package/src/useCurrentState/useCurrentState.ts +22 -0
- package/src/useDateTimeFormat/useDateTimeFormat.test.tsx +47 -0
- package/src/useDateTimeFormat/useDateTimeFormat.ts +22 -0
- package/src/useDebounce/useDebounce.test.ts +49 -0
- package/src/useDebounce/useDebounce.ts +15 -0
- package/src/useDebounceCallback/useDebounceCallback.test.ts +34 -0
- package/src/useDebounceCallback/useDebounceCallback.ts +102 -0
- package/src/useDebugChangedProps/useDebugChangedProps.test.tsx +21 -0
- package/src/useDebugChangedProps/useDebugChangedProps.ts +17 -0
- package/src/useDomObserver/useDomObserver.test.ts +22 -0
- package/src/useDomObserver/useDomObserver.ts +26 -0
- package/src/useDrawer/useDrawer.test.ts +17 -0
- package/src/useDrawer/useDrawer.ts +16 -0
- package/src/useElementSize/useElementSize.test.ts +108 -0
- package/src/useElementSize/useElementSize.ts +34 -0
- package/src/useEventListener/useEventListener.test.ts +128 -0
- package/src/useEventListener/useEventListener.ts +43 -0
- package/src/useFilters/filter.service.ts +86 -0
- package/src/useFilters/filter.ts +152 -0
- package/src/useFilters/useFilters.test.ts +48 -0
- package/src/useFilters/useFilters.ts +44 -0
- package/src/useGenericContext/useGenericContext.test.tsx +19 -0
- package/src/useGenericContext/useGenericContext.ts +18 -0
- package/src/useGeolocation/geolocation.service.ts +66 -0
- package/src/useGeolocation/useGeolocation.test.ts +29 -0
- package/src/useGeolocation/useGeolocation.ts +56 -0
- package/src/useHttp/useHttp.test.tsx +192 -0
- package/src/useHttp/useHttp.ts +52 -0
- package/src/useIdle/useIdle.test.ts +31 -0
- package/src/useIdle/useIdle.ts +79 -0
- package/src/useInfiniteLoader/useInfiniteLoader.test.ts +41 -0
- package/src/useInfiniteLoader/useInfiniteLoader.ts +46 -0
- package/src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts +3 -0
- package/src/useKeyboard/codes.ts +94 -0
- package/src/useKeyboard/helper.ts +31 -0
- package/src/useKeyboard/useKeyboard.test.tsx +163 -0
- package/src/useKeyboard/useKeyboard.ts +91 -0
- package/src/useLabel/useLabel.test.tsx +68 -0
- package/src/useLabel/useLabel.tsx +43 -0
- package/src/useLayout/useLayout.test.tsx +32 -0
- package/src/useLayout/useLayout.tsx +19 -0
- package/src/useModal/useModal.test.ts +19 -0
- package/src/useModal/useModal.ts +16 -0
- package/src/useMounted/useMounted.test.tsx +34 -0
- package/src/useMounted/useMounted.ts +25 -0
- package/src/useNumber/useNumber.test.tsx +36 -0
- package/src/useNumber/useNumber.ts +15 -0
- package/src/usePage/usePage.test.tsx +31 -0
- package/src/usePage/usePage.tsx +19 -0
- package/src/usePagination/pagination.service.ts +23 -0
- package/src/usePagination/usePagination.test.ts +64 -0
- package/src/usePagination/usePagination.ts +47 -0
- package/src/usePortal/usePortal.test.ts +24 -0
- package/src/usePortal/usePortal.ts +34 -0
- package/src/usePrevious/usePrevious.test.ts +21 -0
- package/src/usePrevious/usePrevious.ts +9 -0
- package/src/useRealSize/useRealSize.ts +41 -0
- package/src/useResize/useResize.test.ts +31 -0
- package/src/useResize/useResize.ts +18 -0
- package/src/useSSR/useSSR.test.tsx +16 -0
- package/src/useSSR/useSSR.ts +23 -0
- package/src/useScroll/useScroll.test.tsx +25 -0
- package/src/useScroll/useScroll.ts +30 -0
- package/src/useScrollTo/useScrollTo.test.tsx +105 -0
- package/src/useScrollTo/useScrollTo.ts +41 -0
- package/src/useSearchParams/useSearchParams.test.ts +59 -0
- package/src/useSearchParams/useSearchParams.ts +123 -0
- package/src/useSearchParamsEncoded/useSearchParamsEncoded.test.ts +58 -0
- package/src/useSearchParamsEncoded/useSearchParamsEncoded.ts +202 -0
- package/src/useSorting/useSorting.test.ts +40 -0
- package/src/useSorting/useSorting.ts +99 -0
- package/src/useSwiperIndex/useSwiperIndex.ts +30 -0
- package/src/useSyncedRef/useSyncedRef.test.ts +16 -0
- package/src/useSyncedRef/useSyncedRef.ts +18 -0
- package/src/useUI/useUI.test.ts +16 -0
- package/src/useUI/useUI.ts +44 -0
- package/src/useUnit/useUnit.test.tsx +33 -0
- package/src/useUnit/useUnit.ts +5 -0
- package/src/useUnmountEffect/useUnmountEffect.test.ts +15 -0
- package/src/useUnmountEffect/useUnmountEffect.ts +16 -0
- package/src/useUser/useUser.test.ts +22 -0
- package/src/useUser/useUser.ts +20 -0
- package/src/useWindowSize/useWindowSize.test.ts +48 -0
- 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
|
+
});
|