@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,192 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
3
+ import { getApiUrl } from '@websolutespa/bom-mixer-store';
4
+ import { useHttpDelete, useHttpFetch, useHttpGet, useHttpPatch, useHttpPost, useHttpPut } from './useHttp';
5
+
6
+ const defaultResponse = {
7
+ ok: true,
8
+ status: 200,
9
+ json: () => Promise.resolve(null),
10
+ };
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ function mockFetch(method: string = 'GET', result: any = { message: 'ok' }, overrideResponse: Partial<typeof defaultResponse> = {}) {
14
+ const endpoint = '/endpoint';
15
+ const url = `${getApiUrl(endpoint)}?locale=en&market=ww`;
16
+ const payload = { data: 'data' };
17
+ const response = {
18
+ ...defaultResponse,
19
+ ...overrideResponse,
20
+ json: () => Promise.resolve(result),
21
+ } as Response;
22
+ global.fetch = jest.fn(() =>
23
+ Promise.resolve(response)
24
+ );
25
+ const request: {
26
+ body?: string;
27
+ headers: { [key: string]: string },
28
+ method: string;
29
+ } = {
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ },
33
+ method,
34
+ };
35
+ if (['POST', 'PATCH', 'PUT'].includes(method)) {
36
+ request.body = JSON.stringify(payload);
37
+ }
38
+ return { endpoint, url, payload, result, request, response };
39
+ }
40
+
41
+ describe('useHttp', () => {
42
+ jest.useFakeTimers();
43
+
44
+ const ENV = process.env;
45
+ const FETCH = global.fetch;
46
+
47
+ beforeEach(() => {
48
+ // !!! act error
49
+ jest.spyOn(console, 'error').mockImplementation(() => { });
50
+ jest.resetModules();
51
+ jest.resetAllMocks();
52
+ process.env = {
53
+ NODE_ENV: 'test',
54
+ NEXT_PUBLIC_URL: 'http://localhost:3000',
55
+ NEXT_PUBLIC_MOCK_FALLBACK_URL: 'http://localhost:3000',
56
+ NEXT_PUBLIC_API: '/api',
57
+ NEXT_PUBLIC_GOOGLE_MAP_API_KEY: 'AIzaSyDvGw6iAoKdRv8mmaC9GeT-LWLPQtA8p60',
58
+ STORE_URL: 'http://localhost:4000',
59
+ STORE_API: '/api',
60
+ STORE_BEARER: '7584f15d12972fb1e7695e998dd5e4c754f46c74d06a08e8d76f556adcd045e48cd52bed6faac098784471ac273ee40243194b9cff7eba3c4a768f8f41d2d51959a7f767b943c7f0170f6e5f632db523803c357083bf7a7bf03ee8e0df2d8ce5cd52e0211283b34d7781313da775018a9e950433d2b6faf711c20e5a63b25243',
61
+ STORE_STRATEGY: 'mock',
62
+ SECRET_COOKIE_PASSWORD: '2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8',
63
+ HOOKS_SECRET: '7584f15d12972fb1e7695e998dd5e4',
64
+ };
65
+ });
66
+
67
+ afterAll(() => {
68
+ process.env = ENV;
69
+ global.fetch = FETCH;
70
+ });
71
+
72
+ /*
73
+ !!! todo
74
+ test('useHttpFetch expect loading to be true', async () => {
75
+ const mock = mockFetch();
76
+ const { result } = renderHook(() => useHttpFetch(mock.url));
77
+ await waitFor(() => expect(fetch).toBeCalled());
78
+ expect(fetch).toHaveBeenCalledTimes(1);
79
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
80
+ const [response, loading, error, setError] = result.current;
81
+ expect(loading).toBe(true);
82
+ });
83
+ */
84
+
85
+ test('useHttpFetch expect result ok', async () => {
86
+ const mock = mockFetch();
87
+ const Wrapper = () => {
88
+ const [response, loading, error, setError] = useHttpFetch(mock.url);
89
+ return (
90
+ <>
91
+ {loading && <div data-testid="loading">loading</div>}
92
+ {response && <div data-testid="result">{JSON.stringify(response)}</div>}
93
+ {error && <div data-testid="error">{JSON.stringify(error)}</div>}
94
+ </>
95
+ );
96
+ };
97
+ const { findByTestId } = render(<Wrapper />);
98
+ const loading = await findByTestId('loading');
99
+ expect(loading).toHaveTextContent('loading');
100
+ await waitForElementToBeRemoved(() => screen.getByTestId('loading'));
101
+ await waitFor(() => expect(fetch).toBeCalled());
102
+ expect(fetch).toHaveBeenCalledTimes(1);
103
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
104
+ const result = await waitFor(() => screen.getByTestId('result'));
105
+ expect(result).toHaveTextContent(JSON.stringify(mock.result));
106
+ });
107
+
108
+ /*
109
+ !!! todo
110
+ test('useHttpFetch expect error', async () => {
111
+ const mock = mockFetch(undefined, undefined, { ok: false, status: 500 });
112
+ const Wrapper = () => {
113
+ const [response, loading, error, setError] = useHttpFetch(mock.url);
114
+ return (
115
+ <>
116
+ {loading && <div data-testid="loading">loading</div>}
117
+ {response && <div data-testid="result">{JSON.stringify(response)}</div>}
118
+ {error && <div data-testid="error">{JSON.stringify(error)}</div>}
119
+ </>
120
+ );
121
+ };
122
+ const { findByTestId } = render(<Wrapper />);
123
+ const loading = await findByTestId('loading');
124
+ expect(loading).toHaveTextContent('loading');
125
+ await waitForElementToBeRemoved(() => screen.getByTestId('loading'));
126
+ await waitFor(() => expect(fetch).toBeCalled());
127
+ expect(fetch).toHaveBeenCalledTimes(1);
128
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
129
+ const error = await waitFor(() => screen.getByTestId('error'));
130
+ expect(error).toHaveTextContent('{"ok":false,"status":500}');
131
+ });
132
+ */
133
+
134
+ test('useHttpGet expect result ok', TestMethod('GET'));
135
+
136
+ test('useHttpPost expect result ok', TestMethod('POST'));
137
+
138
+ test('useHttpPatch expect result ok', TestMethod('PATCH'));
139
+
140
+ test('useHttpPut expect result ok', TestMethod('PUT'));
141
+
142
+ test('useHttpDelete expect result ok', TestMethod('DELETE'));
143
+
144
+ function TestMethod(method?: string) {
145
+ const useHook = (mock: ReturnType<typeof mockFetch>) => {
146
+ switch (method) {
147
+ case 'GET':
148
+ // eslint-disable-next-line react-hooks/rules-of-hooks
149
+ return useHttpGet(mock.url);
150
+ case 'POST':
151
+ // eslint-disable-next-line react-hooks/rules-of-hooks
152
+ return useHttpPost(mock.url, mock.payload);
153
+ case 'PATCH':
154
+ // eslint-disable-next-line react-hooks/rules-of-hooks
155
+ return useHttpPatch(mock.url, mock.payload);
156
+ case 'PUT':
157
+ // eslint-disable-next-line react-hooks/rules-of-hooks
158
+ return useHttpPut(mock.url, mock.payload);
159
+ case 'DELETE':
160
+ // eslint-disable-next-line react-hooks/rules-of-hooks
161
+ return useHttpDelete(mock.url);
162
+ default:
163
+ // eslint-disable-next-line react-hooks/rules-of-hooks
164
+ return useHttpFetch(mock.url);
165
+ }
166
+ };
167
+ return async () => {
168
+ const mock = mockFetch(method);
169
+ const Wrapper = () => {
170
+ const [response, loading, error, setError] = useHook(mock);
171
+ return (
172
+ <>
173
+ {loading && <div data-testid="loading">loading</div>}
174
+ {response && <div data-testid="result">{JSON.stringify(response)}</div>}
175
+ {error && <div data-testid="error">{JSON.stringify(error)}</div>}
176
+ </>
177
+ );
178
+ };
179
+ const { findByTestId } = render(<Wrapper />);
180
+ const loading = await findByTestId('loading');
181
+ expect(loading).toHaveTextContent('loading');
182
+ await waitForElementToBeRemoved(() => screen.getByTestId('loading'));
183
+ await waitFor(() => expect(fetch).toBeCalled());
184
+ expect(fetch).toHaveBeenCalledTimes(1);
185
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
186
+ const result = await waitFor(() => screen.getByTestId('result'));
187
+ expect(result).toHaveTextContent(JSON.stringify(mock.result));
188
+ };
189
+ }
190
+
191
+ });
192
+
@@ -0,0 +1,52 @@
1
+ import { FetchRequestOptions, FetchService, httpFetch } from '@websolutespa/bom-core';
2
+ import { Dispatch, useEffect, useState } from 'react';
3
+
4
+ export function useHttpFetch<T = any>(
5
+ pathname: string,
6
+ options: FetchRequestOptions = { method: 'GET' },
7
+ service: FetchService<T> = httpFetch,
8
+ serviceOptions?: any
9
+ ): [T | undefined, boolean, any, Dispatch<any>] {
10
+ const [response, setResponse] = useState<T>();
11
+ const [loading, setLoading] = useState<boolean>(false);
12
+ const [error, setError] = useState<any>(null);
13
+ useEffect(() => {
14
+ // const source = axios.CancelToken.source();
15
+ const fetchData = async () => {
16
+ loading ? null : setLoading(true);
17
+ error ? setError(null) : null;
18
+ try {
19
+ const response = await service(pathname, options, serviceOptions);
20
+ // console.log('useHttp.response', response);
21
+ setResponse(response);
22
+ } catch (error) {
23
+ // console.error(error)
24
+ setError(error);
25
+ }
26
+ setLoading(false);
27
+ };
28
+ fetchData();
29
+ // return () => source.cancel();
30
+ }, [pathname, options.method, options.body]);
31
+ return [response, loading, error, setError];
32
+ }
33
+
34
+ export function useHttpGet<T = any>(pathname: string, options: FetchRequestOptions = {}) {
35
+ return useHttpFetch<T>(pathname, { ...options, method: 'GET' });
36
+ }
37
+
38
+ export function useHttpPost<T = any>(pathname: string, payload: any, options: FetchRequestOptions = {}) {
39
+ return useHttpFetch<T>(pathname, { ...options, method: 'POST', body: payload ? JSON.stringify(payload) : undefined });
40
+ }
41
+
42
+ export function useHttpPut<T = any>(pathname: string, payload: any, options: FetchRequestOptions = {}) {
43
+ return useHttpFetch<T>(pathname, { ...options, method: 'PUT', body: payload ? JSON.stringify(payload) : undefined });
44
+ }
45
+
46
+ export function useHttpPatch<T = any>(pathname: string, payload: any, options: FetchRequestOptions = {}) {
47
+ return useHttpFetch<T>(pathname, { ...options, method: 'PATCH', body: payload ? JSON.stringify(payload) : undefined });
48
+ }
49
+
50
+ export function useHttpDelete<T = any>(pathname: string, options: FetchRequestOptions = {}) {
51
+ return useHttpFetch<T>(pathname, { ...options, method: 'DELETE' });
52
+ }
@@ -0,0 +1,31 @@
1
+ // readme https://testing-library.com/
2
+ import { act, fireEvent, renderHook } from '@testing-library/react';
3
+ import { useIdle } from './useIdle';
4
+
5
+ describe('useIdle', () => {
6
+
7
+ beforeEach(() => {
8
+ jest.useFakeTimers();
9
+ });
10
+
11
+ afterEach(() => {
12
+ jest.useRealTimers();
13
+ });
14
+
15
+ it('should work correctly', async () => {
16
+ const { result } = renderHook(() => useIdle(100));
17
+ expect(result.current).toBe(false);
18
+ fireEvent.mouseMove(document);
19
+ expect(result.current).toBe(false);
20
+ act(() => jest.advanceTimersByTime(100));
21
+ expect(result.current).toBe(true);
22
+ });
23
+
24
+ it('should fire with immediate', async () => {
25
+ const { result } = renderHook(() => useIdle(100, true));
26
+ expect(result.current).toBe(false);
27
+ act(() => jest.advanceTimersByTime(100));
28
+ expect(result.current).toBe(true);
29
+ });
30
+
31
+ });
@@ -0,0 +1,79 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ export function useIdle(delay: number = 3000, immediate: boolean = false) {
4
+
5
+ const [isIdle, setIsIdle] = useState<boolean>(false);
6
+
7
+ // create a new reference to track timer
8
+ const timeoutId = useRef<NodeJS.Timeout>();
9
+
10
+ const goInactive = () => {
11
+ setIsIdle(true);
12
+ };
13
+
14
+ const onStartTimer = () => {
15
+ // wait till delay time before calling goInactive
16
+ timeoutId.current = setTimeout(goInactive, delay);
17
+ };
18
+
19
+ const goActive = () => {
20
+ setIsIdle(false);
21
+ // start the timer to track Inactiveness
22
+ onStartTimer();
23
+ };
24
+
25
+ const onResetTimer = () => {
26
+ // reset the timer and make user active
27
+ clearTimeout(timeoutId.current);
28
+ goActive();
29
+ };
30
+
31
+ const setup = () => {
32
+ document.addEventListener('mousemove', onResetTimer, false);
33
+ document.addEventListener('mousedown', onResetTimer, false);
34
+ document.addEventListener('keypress', onResetTimer, false);
35
+ document.addEventListener('DOMMouseScroll', onResetTimer, false);
36
+ document.addEventListener('mousewheel', onResetTimer, false);
37
+ document.addEventListener('touchmove', onResetTimer, false);
38
+ document.addEventListener('MSPointerMove', onResetTimer, false);
39
+
40
+ // edge case
41
+ // if tab is changed or is out of focus
42
+ window.addEventListener('blur', onStartTimer, false);
43
+ window.addEventListener('focus', onResetTimer, false);
44
+
45
+ if (immediate) {
46
+ onStartTimer();
47
+ }
48
+ };
49
+
50
+ const cleanUp = () => {
51
+ document.removeEventListener('mousemove', onResetTimer);
52
+ document.removeEventListener('mousedown', onResetTimer);
53
+ document.removeEventListener('keypress', onResetTimer);
54
+ document.removeEventListener('DOMMouseScroll', onResetTimer);
55
+ document.removeEventListener('mousewheel', onResetTimer);
56
+ document.removeEventListener('touchmove', onResetTimer);
57
+ document.removeEventListener('MSPointerMove', onResetTimer);
58
+
59
+ // edge case
60
+ // if tab is changed or is out of focus
61
+ window.removeEventListener('blur', onStartTimer);
62
+ window.removeEventListener('focus', onResetTimer);
63
+
64
+ // memory leak
65
+ clearTimeout(timeoutId.current);
66
+ };
67
+
68
+ // assign and remove the listeners
69
+ useEffect(() => {
70
+ setup();
71
+
72
+ return () => {
73
+ cleanUp();
74
+ };
75
+ });
76
+
77
+ // return previous value (happens before update in useEffect above)
78
+ return isIdle;
79
+ }
@@ -0,0 +1,41 @@
1
+ // readme https://testing-library.com/
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { useInfiniteLoader } from './useInfiniteLoader';
4
+
5
+ describe('useInfiniteLoader', () => {
6
+ jest.useFakeTimers();
7
+
8
+ test('should paginate items', () => {
9
+ const items = new Array(100).fill(0).map((_, i) => i + 1);
10
+ const { result } = renderHook(() => useInfiniteLoader(items));
11
+ const [visibleItems, loadMore, hasMore] = result.current;
12
+ expect(visibleItems.length).toEqual(12);
13
+ expect(visibleItems[0]).toEqual(1);
14
+ expect(hasMore).toBe(true);
15
+ });
16
+
17
+ test('should paginate items with params', () => {
18
+ const items = new Array(100).fill(0).map((_, i) => i + 1);
19
+ const { result } = renderHook(() => useInfiniteLoader(items, 20));
20
+ const [visibleItems, loadMore, hasMore] = result.current;
21
+ expect(visibleItems.length).toEqual(20);
22
+ expect(visibleItems[0]).toEqual(1);
23
+ expect(hasMore).toBe(true);
24
+ });
25
+
26
+ test('should call loadMore and paginate items', async () => {
27
+ const items = new Array(100).fill(0).map((_, i) => i + 1);
28
+ const { result } = renderHook(() => useInfiniteLoader(items, 20));
29
+ let [visibleItems, loadMore, hasMore] = result.current;
30
+ expect(visibleItems.length).toEqual(20);
31
+ expect(visibleItems[0]).toEqual(1);
32
+ expect(hasMore).toBe(true);
33
+ act(() => { loadMore(); });
34
+ act(() => jest.advanceTimersByTime(100));
35
+ [visibleItems, loadMore, hasMore] = result.current;
36
+ expect(visibleItems.length).toEqual(40);
37
+ expect(visibleItems.pop()).toEqual(40);
38
+ expect(hasMore).toBe(true);
39
+ });
40
+
41
+ });
@@ -0,0 +1,46 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+
3
+ export function useInfiniteLoader(items: any[], perPage: number = 12, delayInMs = 100): [any[], () => void, boolean] {
4
+
5
+ const total = items.length;
6
+
7
+ const [position, setPosition] = useState(Math.min(total, perPage));
8
+
9
+ const [hasMore, setHasMore] = useState(position < total);
10
+
11
+ const [more, setMore] = useState(0);
12
+
13
+ useEffect(() => {
14
+ const nextPosition = Math.min(total, perPage);
15
+ setPosition(nextPosition);
16
+ setMore(0);
17
+ setHasMore(nextPosition < total);
18
+ }, [items]);
19
+
20
+ const visibleItems = useMemo(() => (
21
+ items.slice(0, position)
22
+ ), [items, position]);
23
+
24
+ const loadMore = () => {
25
+ setMore(more + 1);
26
+ };
27
+
28
+ useEffect(() => {
29
+ // console.log('useInfiniteLoader', hasMore, more, total, position);
30
+ if (hasMore && more > 0) {
31
+ const timer = setTimeout(() => {
32
+ const nextPosition = Math.min(total, position + perPage);
33
+ setPosition(nextPosition);
34
+ setHasMore(nextPosition < total);
35
+ }, delayInMs);
36
+ return () => {
37
+ clearTimeout(timer);
38
+ };
39
+ }
40
+ return;
41
+ }, [more, delayInMs]);
42
+
43
+ return [visibleItems, loadMore, hasMore];
44
+ }
45
+
46
+ export default useInfiniteLoader;
@@ -0,0 +1,3 @@
1
+ import { useEffect, useLayoutEffect } from 'react';
2
+
3
+ export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * KeyBinding Codes
3
+ * The content of this file is based on the design of the open source project "microsoft/vscode",
4
+ * Copyright (c) Microsoft Corporation. All rights reserved.
5
+ *
6
+ * We inherit the KeyMod values from "microsoft/vscode",
7
+ * but use the Browser's KeyboardEvent event implementation, and all values are used only as identification.
8
+ */
9
+
10
+ export enum KeyCode {
11
+ Unknown = 0,
12
+ Backspace = 8,
13
+ Tab = 9,
14
+ Enter = 13,
15
+ Shift = 16,
16
+ Ctrl = 17,
17
+ Alt = 18,
18
+ PauseBreak = 19,
19
+ CapsLock = 20,
20
+ Escape = 27,
21
+ Space = 32,
22
+ PageUp = 33,
23
+ PageDown = 34,
24
+ End = 35,
25
+ Home = 36,
26
+ LeftArrow = 37,
27
+ UpArrow = 38,
28
+ RightArrow = 39,
29
+ DownArrow = 40,
30
+ Insert = 45,
31
+ Delete = 46,
32
+ KEY_0 = 48,
33
+ KEY_1 = 49,
34
+ KEY_2 = 50,
35
+ KEY_3 = 51,
36
+ KEY_4 = 52,
37
+ KEY_5 = 53,
38
+ KEY_6 = 54,
39
+ KEY_7 = 55,
40
+ KEY_8 = 56,
41
+ KEY_9 = 57,
42
+ KEY_A = 65,
43
+ KEY_B = 66,
44
+ KEY_C = 67,
45
+ KEY_D = 68,
46
+ KEY_E = 69,
47
+ KEY_F = 70,
48
+ KEY_G = 71,
49
+ KEY_H = 72,
50
+ KEY_I = 73,
51
+ KEY_J = 74,
52
+ KEY_K = 75,
53
+ KEY_L = 76,
54
+ KEY_M = 77,
55
+ KEY_N = 78,
56
+ KEY_O = 79,
57
+ KEY_P = 80,
58
+ KEY_Q = 81,
59
+ KEY_R = 82,
60
+ KEY_S = 83,
61
+ KEY_T = 84,
62
+ KEY_U = 85,
63
+ KEY_V = 86,
64
+ KEY_W = 87,
65
+ KEY_X = 88,
66
+ KEY_Y = 89,
67
+ KEY_Z = 90,
68
+ Meta = 91,
69
+ F1 = 112,
70
+ F2 = 113,
71
+ F3 = 114,
72
+ F4 = 115,
73
+ F5 = 116,
74
+ F6 = 117,
75
+ F7 = 118,
76
+ F8 = 119,
77
+ F9 = 120,
78
+ F10 = 121,
79
+ F11 = 122,
80
+ F12 = 123,
81
+ NumLock = 144,
82
+ ScrollLock = 145,
83
+ Equal = 187,
84
+ Minus = 189,
85
+ Backquote = 192,
86
+ Backslash = 220,
87
+ }
88
+
89
+ export enum KeyMod {
90
+ CtrlCmd = (1 << 11) >>> 0,
91
+ Shift = (1 << 10) >>> 0,
92
+ Alt = (1 << 9) >>> 0,
93
+ WinCtrl = (1 << 8) >>> 0,
94
+ }
@@ -0,0 +1,31 @@
1
+ import { detectIsBrowser } from '../useSSR/useSSR';
2
+ import { KeyMod } from './codes';
3
+
4
+ export function getCtrlKeysByPlatform(): Record<string, 'metaKey' | 'ctrlKey'> {
5
+ return {
6
+ CtrlCmd: isMac() ? 'metaKey' : 'ctrlKey',
7
+ WinCtrl: isMac() ? 'ctrlKey' : 'metaKey',
8
+ };
9
+ }
10
+
11
+ export function getActiveModMap(bindings: number[]): Record<keyof typeof KeyMod, boolean> {
12
+ const modBindings = bindings.filter((item: number) => !!KeyMod[item]);
13
+ const activeModMap: Record<keyof typeof KeyMod, boolean> = {
14
+ CtrlCmd: false,
15
+ Shift: false,
16
+ Alt: false,
17
+ WinCtrl: false,
18
+ };
19
+ modBindings.forEach(code => {
20
+ const modKey = KeyMod[code] as keyof typeof KeyMod;
21
+ activeModMap[modKey] = true;
22
+ });
23
+ return activeModMap;
24
+ }
25
+
26
+ export function isMac(): boolean {
27
+ if (!detectIsBrowser()) {
28
+ return false;
29
+ }
30
+ return navigator.platform.toUpperCase().indexOf('MAC') >= 0;
31
+ }