@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @websolutespa/bom-mixer-hooks
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Modified: useBreakpoint theme mediaQuery.
8
+
9
+ ## 0.2.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Added: source files.
14
+
3
15
  ## 0.2.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.js CHANGED
@@ -380,7 +380,7 @@ function useBreakpoint() {
380
380
  throw new Error("useBreakpoint must be used within ThemeProvider");
381
381
  }
382
382
  const breakpoints = Object.keys(theme.mediaQuery);
383
- const values = Object.values(theme.mediaQuery);
383
+ const values = Object.values(theme.mediaQuery).map((x) => parseInt(String(x)));
384
384
  const breakpoint = (0, import_react7.useMemo)(() => {
385
385
  const breakpoint2 = values.reduce((p, c, i) => {
386
386
  if (c < windowSize.width) {
package/dist/index.mjs CHANGED
@@ -263,7 +263,7 @@ function useBreakpoint() {
263
263
  throw new Error("useBreakpoint must be used within ThemeProvider");
264
264
  }
265
265
  const breakpoints = Object.keys(theme.mediaQuery);
266
- const values = Object.values(theme.mediaQuery);
266
+ const values = Object.values(theme.mediaQuery).map((x) => parseInt(String(x)));
267
267
  const breakpoint = useMemo(() => {
268
268
  const breakpoint2 = values.reduce((p, c, i) => {
269
269
  if (c < windowSize.width) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@websolutespa/bom-mixer-hooks",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Mixer Hooks module of the BOM Repository",
5
5
  "keywords": [
6
6
  "bom",
@@ -58,14 +58,16 @@
58
58
  "sideEffects": false,
59
59
  "prepack": {
60
60
  "types": "./dist/index.d.ts",
61
- "main": "./dist/index.js",
62
- "module": "./dist/index.mjs"
61
+ "main": "./dist/index.mjs",
62
+ "module": "./dist/index.mjs",
63
+ "source": "./src/index.ts"
63
64
  },
64
65
  "postpack": {
65
66
  "types": "./src/index.ts",
66
67
  "main": "./src/index.ts"
67
68
  },
68
69
  "types": "./dist/index.d.ts",
69
- "main": "./dist/index.js",
70
- "module": "./dist/index.mjs"
70
+ "main": "./dist/index.mjs",
71
+ "module": "./dist/index.mjs",
72
+ "source": "./src/index.ts"
71
73
  }
package/src/index.ts ADDED
@@ -0,0 +1,60 @@
1
+ export * from './useApi/useApi';
2
+ export * from './useBodyScroll/useBodyScroll';
3
+ export * from './useBreakpoint/useBreakpoint';
4
+ export * from './useCart/useCart';
5
+ export * from './useCheckout/useCheckout';
6
+ export * from './useClickAnyWhere/useClickAnyWhere';
7
+ export * from './useClickOut/useClickOut';
8
+ export * from './useCurrency/useCurrency';
9
+ export * from './useCurrentState/useCurrentState';
10
+ export * from './useDateTimeFormat/useDateTimeFormat';
11
+ export * from './useDebounce/useDebounce';
12
+ export * from './useDebounceCallback/useDebounceCallback';
13
+ export * from './useDebugChangedProps/useDebugChangedProps';
14
+ export * from './useDomObserver/useDomObserver';
15
+ export * from './useDrawer/useDrawer';
16
+ export * from './useElementSize/useElementSize';
17
+ export * from './useEventListener/useEventListener';
18
+ export * from './useFilters/filter';
19
+ export * from './useFilters/filter.service';
20
+ export * from './useFilters/useFilters';
21
+ export * from './useGenericContext/useGenericContext';
22
+ export * from './useGeolocation/geolocation.service';
23
+ export * from './useGeolocation/useGeolocation';
24
+ export * from './useIdle/useIdle';
25
+ export * from './useInfiniteLoader/useInfiniteLoader';
26
+ export * from './useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
27
+ export * from './useKeyboard/useKeyboard';
28
+ export * from './useLabel/useLabel';
29
+ export * from './useLayout/useLayout';
30
+ export * from './useModal/useModal';
31
+ export * from './useMounted/useMounted';
32
+ export * from './useNumber/useNumber';
33
+ export * from './usePage/usePage';
34
+ export * from './usePagination/pagination.service';
35
+ export * from './usePagination/usePagination';
36
+ export * from './usePortal/usePortal';
37
+ export * from './usePrevious/usePrevious';
38
+ export * from './useRealSize/useRealSize';
39
+ export * from './useResize/useResize';
40
+ export * from './useScroll/useScroll';
41
+ export * from './useScrollTo/useScrollTo';
42
+ export * from './useSearchParams/useSearchParams';
43
+ export * from './useSearchParamsEncoded/useSearchParamsEncoded';
44
+ export * from './useSorting/useSorting';
45
+ export * from './useSSR/useSSR';
46
+ export * from './useSwiperIndex/useSwiperIndex';
47
+ export * from './useSyncedRef/useSyncedRef';
48
+ export * from './useUI/useUI';
49
+ export * from './useUnit/useUnit';
50
+ export * from './useUnmountEffect/useUnmountEffect';
51
+ export * from './useUser/useUser';
52
+ export * from './useWindowSize/useWindowSize';
53
+
54
+ /**
55
+ *
56
+ * useful hooks
57
+ * https://streamich.github.io/react-use
58
+ * https://github.com/react-hookz/web
59
+ *
60
+ */
@@ -0,0 +1,193 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, renderHook, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
3
+ import { LayoutDefaults } from '@websolutespa/bom-mixer-mock';
4
+ import { getApiUrl } from '@websolutespa/bom-mixer-store';
5
+ import { LayoutProvider } from '../useLayout/useLayout';
6
+ import { useApi, useApiDelete, useApiFetch, useApiGet, useApiPatch, useApiPost, useApiPut } from './useApi';
7
+
8
+ const wrapper = (props: { children: React.ReactNode }) => (
9
+ <LayoutProvider layout={LayoutDefaults}>{props.children}</LayoutProvider>
10
+ );
11
+
12
+ const defaultResponse = {
13
+ ok: true,
14
+ status: 200,
15
+ json: () => Promise.resolve(null),
16
+ };
17
+
18
+ function mockFetch(
19
+ method: string = 'GET',
20
+ result: any = { message: 'ok' },
21
+ overrideResponse: Partial<typeof defaultResponse> = {}
22
+ ) {
23
+ const endpoint = '/endpoint';
24
+ const url = `${getApiUrl(endpoint)}?locale=en&market=ww`;
25
+ const payload = { data: 'data' };
26
+ const response = {
27
+ ...defaultResponse,
28
+ ...overrideResponse,
29
+ json: () => Promise.resolve(result),
30
+ } as Response;
31
+ global.fetch = jest.fn(() =>
32
+ Promise.resolve(response)
33
+ );
34
+ const request: {
35
+ body?: string;
36
+ headers: { [key: string]: string },
37
+ method: string;
38
+ } = {
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ method,
43
+ };
44
+ if (['POST', 'PATCH', 'PUT'].includes(method)) {
45
+ request.body = JSON.stringify(payload);
46
+ }
47
+ return { endpoint, url, payload, result, request, response };
48
+ }
49
+
50
+ describe('useApi', () => {
51
+ const ENV = process.env;
52
+ const FETCH = global.fetch;
53
+
54
+ beforeEach(() => {
55
+ jest.resetModules();
56
+ jest.resetAllMocks();
57
+ jest.spyOn(console, 'error').mockImplementation(() => { });
58
+ process.env = {
59
+ NODE_ENV: 'test',
60
+ NEXT_PUBLIC_URL: 'http://localhost:3000',
61
+ NEXT_PUBLIC_MOCK_FALLBACK_URL: 'http://localhost:3000',
62
+ NEXT_PUBLIC_API: '/api',
63
+ NEXT_PUBLIC_GOOGLE_MAP_API_KEY: 'AIzaSyDvGw6iAoKdRv8mmaC9GeT-LWLPQtA8p60',
64
+ STORE_URL: 'http://localhost:4000',
65
+ STORE_API: '/api',
66
+ STORE_BEARER: '7584f15d12972fb1e7695e998dd5e4c754f46c74d06a08e8d76f556adcd045e48cd52bed6faac098784471ac273ee40243194b9cff7eba3c4a768f8f41d2d51959a7f767b943c7f0170f6e5f632db523803c357083bf7a7bf03ee8e0df2d8ce5cd52e0211283b34d7781313da775018a9e950433d2b6faf711c20e5a63b25243',
67
+ STORE_STRATEGY: 'mock',
68
+ SECRET_COOKIE_PASSWORD: '2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8',
69
+ HOOKS_SECRET: '7584f15d12972fb1e7695e998dd5e4',
70
+ };
71
+ });
72
+
73
+ afterAll(() => {
74
+ process.env = ENV;
75
+ global.fetch = FETCH;
76
+ });
77
+
78
+ test('should throw without provider', () => {
79
+ expect(() => {
80
+ renderHook(() => useApi());
81
+ }).toThrow('useGenericContext must be used within a Provider');
82
+ });
83
+
84
+ test('should not throw with provider', () => {
85
+ expect(() => {
86
+ renderHook(() => useApi(), { wrapper });
87
+ }).not.toThrow();
88
+ });
89
+
90
+ test('api.get should return ok', async () => {
91
+ const { result } = renderHook(() => useApi(), { wrapper });
92
+ const mock = mockFetch();
93
+ const response = await result.current.get(mock.endpoint);
94
+ expect(fetch).toHaveBeenCalledTimes(1);
95
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
96
+ expect(response).toBe(mock.result);
97
+ });
98
+
99
+ test('api.post should return ok', async () => {
100
+ const { result } = renderHook(() => useApi(), { wrapper });
101
+ const mock = mockFetch('POST');
102
+ const response = await result.current.post(mock.endpoint, mock.payload);
103
+ expect(fetch).toHaveBeenCalledTimes(1);
104
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
105
+ expect(response).toBe(mock.result);
106
+ });
107
+
108
+ test('api.patch should return ok', async () => {
109
+ const { result } = renderHook(() => useApi(), { wrapper });
110
+ const mock = mockFetch('PATCH');
111
+ const response = await result.current.patch(mock.endpoint, mock.payload);
112
+ expect(fetch).toHaveBeenCalledTimes(1);
113
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
114
+ expect(response).toBe(mock.result);
115
+ });
116
+
117
+ test('api.put should return ok', async () => {
118
+ const { result } = renderHook(() => useApi(), { wrapper });
119
+ const mock = mockFetch('PUT');
120
+ const response = await result.current.put(mock.endpoint, mock.payload);
121
+ expect(fetch).toHaveBeenCalledTimes(1);
122
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
123
+ expect(response).toBe(mock.result);
124
+ });
125
+
126
+ test('api.delete should return ok', async () => {
127
+ const { result } = renderHook(() => useApi(), { wrapper });
128
+ const mock = mockFetch('DELETE');
129
+ const response = await result.current.delete(mock.endpoint);
130
+ expect(fetch).toHaveBeenCalledTimes(1);
131
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
132
+ expect(response).toBe(mock.result);
133
+ });
134
+
135
+ test('useApiGet expect result ok', TestMethod('GET'));
136
+
137
+ test('useApiPost expect result ok', TestMethod('POST'));
138
+
139
+ test('useApiPatch expect result ok', TestMethod('PATCH'));
140
+
141
+ test('useApiPut expect result ok', TestMethod('PUT'));
142
+
143
+ test('useApiDelete expect result ok', TestMethod('DELETE'));
144
+
145
+ function TestMethod(method?: string) {
146
+ const useHook = (mock: ReturnType<typeof mockFetch>) => {
147
+ switch (method) {
148
+ case 'GET':
149
+ // eslint-disable-next-line react-hooks/rules-of-hooks
150
+ return useApiGet(mock.endpoint);
151
+ case 'POST':
152
+ // eslint-disable-next-line react-hooks/rules-of-hooks
153
+ return useApiPost(mock.endpoint, mock.payload);
154
+ case 'PATCH':
155
+ // eslint-disable-next-line react-hooks/rules-of-hooks
156
+ return useApiPatch(mock.endpoint, mock.payload);
157
+ case 'PUT':
158
+ // eslint-disable-next-line react-hooks/rules-of-hooks
159
+ return useApiPut(mock.endpoint, mock.payload);
160
+ case 'DELETE':
161
+ // eslint-disable-next-line react-hooks/rules-of-hooks
162
+ return useApiDelete(mock.endpoint);
163
+ default:
164
+ // eslint-disable-next-line react-hooks/rules-of-hooks
165
+ return useApiFetch(mock.endpoint);
166
+ }
167
+ };
168
+ return async () => {
169
+ const mock = mockFetch(method);
170
+ const Wrapper = () => {
171
+ const [response, loading, error, setError] = useHook(mock);
172
+ return (
173
+ <>
174
+ {loading && <div data-testid="loading">loading</div>}
175
+ {response && <div data-testid="result">{JSON.stringify(response)}</div>}
176
+ {error && <div data-testid="error">{JSON.stringify(error)}</div>}
177
+ </>
178
+ );
179
+ };
180
+ render(
181
+ <LayoutProvider layout={LayoutDefaults}><Wrapper /></LayoutProvider>
182
+ );
183
+ expect(screen.getByTestId('loading')).toHaveTextContent('loading');
184
+ await waitForElementToBeRemoved(() => screen.getByTestId('loading'));
185
+ expect(fetch).toHaveBeenCalledTimes(1);
186
+ expect(fetch).toHaveBeenCalledWith(mock.url, mock.request);
187
+ const result = await waitFor(() => screen.getByTestId('result'));
188
+ expect(result).toHaveTextContent(JSON.stringify(mock.result));
189
+ };
190
+ }
191
+
192
+ });
193
+
@@ -0,0 +1,58 @@
1
+ import { FetchRequestOptions } from '@websolutespa/bom-core';
2
+ import { apiDelete, apiFetch, apiGet, apiPatch, apiPost, apiPut } from '@websolutespa/bom-mixer-store';
3
+ import { useHttpFetch } from '../useHttp/useHttp';
4
+ import { useLayout } from '../useLayout/useLayout';
5
+
6
+ export function useApiFetch<T = any>(pathname: string, options: FetchRequestOptions = {}) {
7
+ const layout = useLayout();
8
+ return useHttpFetch<T>(pathname, options, apiFetch, layout);
9
+ }
10
+
11
+ export function useApiGet<T = any>(pathname: string, options: FetchRequestOptions = {}) {
12
+ return useApiFetch<T>(pathname, { ...options, method: 'GET' });
13
+ }
14
+
15
+ export function useApiPost<T = any>(pathname: string, payload: unknown, options: FetchRequestOptions = {}) {
16
+ return useApiFetch<T>(pathname, { ...options, method: 'POST', body: payload ? JSON.stringify(payload) : undefined });
17
+ }
18
+
19
+ export function useApiPut<T = any>(pathname: string, payload: unknown, options: FetchRequestOptions = {}) {
20
+ return useApiFetch<T>(pathname, { ...options, method: 'PUT', body: payload ? JSON.stringify(payload) : undefined });
21
+ }
22
+
23
+ export function useApiPatch<T = any>(pathname: string, payload: unknown, options: FetchRequestOptions = {}) {
24
+ return useApiFetch<T>(pathname, { ...options, method: 'PATCH', body: payload ? JSON.stringify(payload) : undefined });
25
+ }
26
+
27
+ export function useApiDelete<T = any>(pathname: string, options: FetchRequestOptions = {}) {
28
+ return useApiFetch<T>(pathname, { ...options, method: 'DELETE' });
29
+ }
30
+
31
+ type Api = {
32
+ get: <T = any>(pathname: string, options?: FetchRequestOptions) => Promise<T>;
33
+ post: <T = any>(pathname: string, payload?: unknown, options?: FetchRequestOptions) => Promise<T>;
34
+ put: <T = any>(pathname: string, payload?: unknown, options?: FetchRequestOptions) => Promise<T>;
35
+ patch: <T = any>(pathname: string, payload?: unknown, options?: FetchRequestOptions) => Promise<T>;
36
+ delete: <T = any>(pathname: string, options?: FetchRequestOptions) => Promise<T>;
37
+ };
38
+
39
+ export function useApi(): Api {
40
+ const layout = useLayout();
41
+ return {
42
+ get: async <T = any>(pathname: string, options: FetchRequestOptions = {}) => {
43
+ return apiGet<T>(pathname, options, layout);
44
+ },
45
+ post: async <T = any>(pathname: string, payload?: unknown, options: FetchRequestOptions = {}) => {
46
+ return apiPost<T>(pathname, payload, options, layout);
47
+ },
48
+ put: async <T = any>(pathname: string, payload?: unknown, options: FetchRequestOptions = {}) => {
49
+ return apiPut<T>(pathname, payload, options, layout);
50
+ },
51
+ patch: async <T = any>(pathname: string, payload?: unknown, options: FetchRequestOptions = {}) => {
52
+ return apiPatch<T>(pathname, payload, options, layout);
53
+ },
54
+ delete: async <T = any>(pathname: string, options: FetchRequestOptions = {}) => {
55
+ return apiDelete<T>(pathname, options, layout);
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,112 @@
1
+ // readme https://testing-library.com/
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { createRef, RefObject } from 'react';
4
+ import { useBodyScroll } from './useBodyScroll';
5
+
6
+ describe('useBodyScroll', () => {
7
+ jest.useFakeTimers();
8
+
9
+ it('should work correctly', () => {
10
+ const ref = createRef<HTMLDivElement>();
11
+ (ref as any).current = document.createElement('div');
12
+ const { result } = renderHook(() => useBodyScroll(ref));
13
+ let [hidden, setHidden] = result.current;
14
+ expect(hidden).toBe(false);
15
+ act(() => setHidden(true));
16
+ [hidden, setHidden] = result.current;
17
+ expect(hidden).toBe(true);
18
+ });
19
+
20
+ it('should set overflow', async () => {
21
+ const ref = createRef<HTMLDivElement>();
22
+ (ref as any).current = document.createElement('div');
23
+ const element = ref.current as HTMLDivElement;
24
+ const { result } = renderHook(() => useBodyScroll(ref));
25
+ act(() => result.current[1](true));
26
+ expect(element.style.overflow).toEqual('hidden');
27
+ act(() => result.current[1](false));
28
+ act(() => jest.advanceTimersByTime(10));
29
+ expect(element.style.overflow).not.toEqual('hidden');
30
+ });
31
+
32
+ it('the last value of overflow should be recovered after setHidden', async () => {
33
+ const ref = createRef<HTMLDivElement>();
34
+ const div = document.createElement('div');
35
+ div.style.overflow = 'scroll';
36
+ (ref as any).current = div;
37
+ const element = ref.current as HTMLDivElement;
38
+ const { result } = renderHook(() => useBodyScroll(ref));
39
+ let [hidden, setHidden] = result.current;
40
+ expect(element.style.overflow).toEqual('scroll');
41
+ act(() => setHidden(true));
42
+ expect(element.style.overflow).toEqual('hidden');
43
+ [hidden, setHidden] = result.current;
44
+ act(() => setHidden(false));
45
+ act(() => jest.advanceTimersByTime(10));
46
+ expect(element.style.overflow).toEqual('scroll');
47
+ });
48
+
49
+ it('should work correctly with multiple element', async () => {
50
+ const ref1 = createRef<HTMLDivElement>();
51
+ (ref1 as any).current = document.createElement('div');
52
+ const element1 = ref1.current as HTMLDivElement;
53
+ const { result } = renderHook(() => useBodyScroll(ref1));
54
+ const ref2 = createRef<HTMLDivElement>();
55
+ (ref2 as any).current = document.createElement('div');
56
+ const element2 = ref2.current as HTMLDivElement;
57
+ const { result: result2 } = renderHook(() => useBodyScroll(ref2));
58
+ act(() => result.current[1](true));
59
+ act(() => result2.current[1](true));
60
+ expect(element1.style.overflow).toEqual('hidden');
61
+ expect(element2.style.overflow).toEqual('hidden');
62
+ act(() => result.current[1](false));
63
+ act(() => result2.current[1](false));
64
+ act(() => jest.advanceTimersByTime(10));
65
+ expect(element1.style.overflow).toEqual('');
66
+ expect(element2.style.overflow).toEqual('');
67
+ });
68
+
69
+ it('should work correctly with options', async () => {
70
+ const ref = createRef<HTMLDivElement>();
71
+ (ref as any).current = document.createElement('div');
72
+ const element = ref.current as HTMLDivElement;
73
+ const { result } = renderHook(() => useBodyScroll(ref, { delayReset: 300 }));
74
+ const [hidden, setHidden] = result.current;
75
+ act(() => setHidden(true));
76
+ expect(element.style.overflow).toEqual('hidden');
77
+ act(() => setHidden(false));
78
+ act(() => jest.advanceTimersByTime(10));
79
+ expect(element.style.overflow).toEqual('hidden');
80
+ act(() => jest.advanceTimersByTime(100));
81
+ expect(element.style.overflow).toEqual('hidden');
82
+ act(() => jest.advanceTimersByTime(250));
83
+ expect(element.style.overflow).not.toEqual('hidden');
84
+ });
85
+
86
+ it('should work correctly when set element repeatedly', () => {
87
+ let _ref: RefObject<HTMLDivElement> | null = null;
88
+ const ref = createRef<HTMLDivElement>();
89
+ (ref as any).current = document.createElement('div');
90
+ _ref = ref;
91
+ const element = ref.current as HTMLDivElement;
92
+ const { result, rerender } = renderHook(() => useBodyScroll(_ref));
93
+ let [hidden, setHidden] = result.current;
94
+ act(() => setHidden(true));
95
+ expect(element.style.overflow).toEqual('hidden');
96
+ // Force tigger rerender at the same value
97
+ _ref = createRef<HTMLDivElement>();
98
+ rerender();
99
+ _ref = ref;
100
+ rerender();
101
+ [hidden, setHidden] = result.current;
102
+ act(() => setHidden(true));
103
+ expect(element.style.overflow).toEqual('hidden');
104
+ });
105
+
106
+ it('should set body when missing all params', () => {
107
+ const { result } = renderHook(() => useBodyScroll());
108
+ const [hidden, setHidden] = result.current;
109
+ act(() => setHidden(true));
110
+ expect(document.body.style.overflow).toEqual('hidden');
111
+ });
112
+ });
@@ -0,0 +1,89 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react';
3
+
4
+ const defaultOptions: BodyScrollOptions = {
5
+ scrollLayer: false,
6
+ delayReset: 0,
7
+ };
8
+
9
+ const elementStack = new Map<HTMLElement, ElementStackItem>();
10
+
11
+ export function useBodyScroll(elementRef?: RefObject<HTMLElement> | null, options?: BodyScrollOptions): [boolean, Dispatch<SetStateAction<boolean>>] {
12
+ if (typeof document === 'undefined') {
13
+ return [false, (t: boolean) => t] as any;
14
+ }
15
+ elementRef = elementRef || useRef<HTMLElement>(document.body);
16
+ const [hidden, setHidden] = useState<boolean>(false);
17
+ const safeOptions = {
18
+ ...defaultOptions,
19
+ ...(options || {}),
20
+ };
21
+
22
+ useEffect(() => {
23
+ if (!elementRef || !elementRef.current) {
24
+ return;
25
+ }
26
+ const lastOverflow = elementRef.current.style.overflow;
27
+ if (hidden) {
28
+ if (elementStack.has(elementRef.current)) {
29
+ return;
30
+ }
31
+ const paddingRight = getOwnerPaddingRight(elementRef.current);
32
+ const scrollbarWidth = getOwnerScrollbarWidth(elementRef.current);
33
+ // console.log('paddingRight', paddingRight, 'scrollbarWidth', scrollbarWidth);
34
+ elementStack.set(elementRef.current, {
35
+ overflow: lastOverflow,
36
+ paddingRight: elementRef.current.style.paddingRight,
37
+ });
38
+ elementRef.current.style.setProperty('--locked-padding-right', `${paddingRight + scrollbarWidth}px`);
39
+ elementRef.current.style.overflow = 'hidden';
40
+ elementRef.current.style.paddingRight = 'var(--locked-padding-right)';
41
+ return;
42
+ }
43
+
44
+ // reset element overflow
45
+ if (!elementStack.has(elementRef.current)) {
46
+ return;
47
+ }
48
+
49
+ const reset = (element: HTMLElement) => {
50
+ const store = elementStack.get(element) as ElementStackItem;
51
+ if (!store) {
52
+ return;
53
+ }
54
+ element.style.setProperty('--locked-padding-right', '0');
55
+ element.style.overflow = store.overflow;
56
+ element.style.paddingRight = store.paddingRight;
57
+ elementStack.delete(element);
58
+ };
59
+
60
+ const timer = window.setTimeout(() => {
61
+ reset(elementRef!.current!);
62
+ window.clearTimeout(timer);
63
+ }, safeOptions.delayReset);
64
+
65
+ }, [hidden, elementRef]);
66
+
67
+ return [hidden, setHidden];
68
+ }
69
+
70
+ export function getOwnerPaddingRight(element: Element): number {
71
+ const owner = element?.ownerDocument || document;
72
+ const view = owner.defaultView || window;
73
+ return Number.parseInt(view.getComputedStyle(element).paddingRight, 10) || 0;
74
+ }
75
+
76
+ export function getOwnerScrollbarWidth(element: Element): number {
77
+ const doc = element?.ownerDocument || document;
78
+ return Math.abs(window.innerWidth - doc.documentElement.clientWidth);
79
+ }
80
+
81
+ export type ElementStackItem = {
82
+ overflow: string;
83
+ paddingRight: string;
84
+ };
85
+
86
+ export type BodyScrollOptions = {
87
+ scrollLayer?: boolean;
88
+ delayReset?: number;
89
+ };
@@ -0,0 +1,45 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { ThemeDefaults } from '@websolutespa/bom-mixer-mock';
3
+ import { ThemeProvider } from 'styled-components';
4
+ import { useBreakpoint } from './useBreakpoint';
5
+
6
+ const windowResize = (dimension: 'width' | 'height', value: number): void => {
7
+ if (dimension === 'width') {
8
+ window.innerWidth = value;
9
+ }
10
+ if (dimension === 'height') {
11
+ window.innerHeight = value;
12
+ }
13
+ window.dispatchEvent(new Event('resize'));
14
+ };
15
+
16
+ const wrapper = (props: { children: React.ReactNode }) => (
17
+ <ThemeProvider theme={ThemeDefaults}>{props.children}</ThemeProvider>
18
+ );
19
+
20
+ describe('useBreakpoint', () => {
21
+ beforeEach(() => {
22
+ jest.spyOn(console, 'error').mockImplementation(() => { });
23
+ });
24
+
25
+ test('should throw without provider', () => {
26
+ expect(() => {
27
+ renderHook(() => useBreakpoint());
28
+ }).toThrow('useBreakpoint must be used within ThemeProvider');
29
+ });
30
+
31
+ test('should not throw with provider', () => {
32
+ expect(() => {
33
+ renderHook(() => useBreakpoint(), { wrapper });
34
+ }).not.toThrow();
35
+ });
36
+
37
+ it('should work correctly', () => {
38
+ const { result } = renderHook(() => useBreakpoint(), { wrapper });
39
+ expect(result.current).toStrictEqual({ key: 'sm', min: 768, max: 1024 });
40
+ act(() => {
41
+ windowResize('width', 1920);
42
+ });
43
+ expect(result.current).toStrictEqual({ key: 'xl', min: 1600, max: Number.POSITIVE_INFINITY });
44
+ });
45
+ });
@@ -0,0 +1,44 @@
1
+ import { useContext, useMemo } from 'react';
2
+ import { ThemeContext } from 'styled-components';
3
+ import { useWindowSize } from '../useWindowSize/useWindowSize';
4
+
5
+ type BreakpointType = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
6
+
7
+ type Breakpoint = {
8
+ key: BreakpointType;
9
+ min: number;
10
+ max: number;
11
+ };
12
+
13
+ export function useBreakpoint(): Breakpoint {
14
+ // todo
15
+ const windowSize = useWindowSize();
16
+
17
+ // const [breakpoint, setBreakpoint] = useState<Breakpoint>({ key: 'xs', min: 0, max: 0 });
18
+
19
+ const theme = useContext(ThemeContext);
20
+ if (!theme) {
21
+ throw new Error('useBreakpoint must be used within ThemeProvider');
22
+ }
23
+
24
+ const breakpoints: BreakpointType[] = Object.keys(theme.mediaQuery) as BreakpointType[];
25
+ const values: number[] = Object.values(theme.mediaQuery).map(x => parseInt(String(x)));
26
+
27
+ const breakpoint = useMemo(() => {
28
+ const breakpoint = values.reduce((p, c, i) => {
29
+ if (c < windowSize.width) {
30
+ p.key = breakpoints[i];
31
+ p.min = c;
32
+ if (i + 1 < values.length) {
33
+ p.max = values[i + 1];
34
+ } else {
35
+ p.max = Number.POSITIVE_INFINITY;
36
+ }
37
+ }
38
+ return p;
39
+ }, { key: 'xs' as BreakpointType, min: -Number.NEGATIVE_INFINITY, max: -Number.NEGATIVE_INFINITY });
40
+ return breakpoint;
41
+ }, [windowSize.width]);
42
+
43
+ return breakpoint;
44
+ }