@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,163 @@
|
|
|
1
|
+
import { fireEvent, render, renderHook, screen } from '@testing-library/react';
|
|
2
|
+
import { KeyboardResult, KeyCode, KeyMod, useKeyboard } from './useKeyboard';
|
|
3
|
+
|
|
4
|
+
describe('useKeyboard', () => {
|
|
5
|
+
it('should work correctly', () => {
|
|
6
|
+
let code = null;
|
|
7
|
+
const handler = jest.fn().mockImplementation(e => {
|
|
8
|
+
code = e.keyCode;
|
|
9
|
+
});
|
|
10
|
+
renderHook(() => useKeyboard(handler, KeyCode.KEY_H));
|
|
11
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_H }));
|
|
12
|
+
expect(handler).toBeCalledTimes(1);
|
|
13
|
+
expect(code).toEqual(KeyCode.KEY_H);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should not trigger handler', () => {
|
|
17
|
+
const handler = jest.fn().mockImplementation(() => { });
|
|
18
|
+
renderHook(() => useKeyboard(handler, [KeyCode.KEY_0]));
|
|
19
|
+
const event = new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_1 });
|
|
20
|
+
document.dispatchEvent(event);
|
|
21
|
+
expect(handler).not.toBeCalled();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should trigger with command key', () => {
|
|
25
|
+
const handler = jest.fn().mockImplementation(() => { });
|
|
26
|
+
renderHook(() => useKeyboard(handler, [KeyCode.KEY_A, KeyMod.Shift]));
|
|
27
|
+
const event = new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_A });
|
|
28
|
+
document.dispatchEvent(event);
|
|
29
|
+
expect(handler).not.toBeCalled();
|
|
30
|
+
const event2 = new KeyboardEvent('keydown', {
|
|
31
|
+
keyCode: KeyCode.KEY_A,
|
|
32
|
+
shiftKey: true,
|
|
33
|
+
});
|
|
34
|
+
document.dispatchEvent(event2);
|
|
35
|
+
expect(handler).toBeCalledTimes(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should ignore command when code does not exist', () => {
|
|
39
|
+
const handler = jest.fn().mockImplementation(() => { });
|
|
40
|
+
renderHook(() => useKeyboard(handler, [KeyCode.KEY_A, 12345]));
|
|
41
|
+
const event = new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_A });
|
|
42
|
+
document.dispatchEvent(event);
|
|
43
|
+
expect(handler).toBeCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should work with each command', () => {
|
|
47
|
+
const handler = jest.fn().mockImplementation(() => { });
|
|
48
|
+
renderHook(() =>
|
|
49
|
+
useKeyboard(handler, [KeyCode.KEY_A, KeyMod.Alt, KeyMod.CtrlCmd, KeyMod.WinCtrl]),
|
|
50
|
+
);
|
|
51
|
+
document.dispatchEvent(
|
|
52
|
+
new KeyboardEvent('keydown', {
|
|
53
|
+
keyCode: KeyCode.KEY_A,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
expect(handler).not.toBeCalled();
|
|
57
|
+
document.dispatchEvent(
|
|
58
|
+
new KeyboardEvent('keydown', {
|
|
59
|
+
keyCode: KeyCode.KEY_A,
|
|
60
|
+
altKey: true,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
expect(handler).not.toBeCalled();
|
|
64
|
+
document.dispatchEvent(
|
|
65
|
+
new KeyboardEvent('keydown', {
|
|
66
|
+
keyCode: KeyCode.KEY_A,
|
|
67
|
+
altKey: true,
|
|
68
|
+
ctrlKey: true,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
expect(handler).not.toBeCalled();
|
|
72
|
+
document.dispatchEvent(
|
|
73
|
+
new KeyboardEvent('keydown', {
|
|
74
|
+
keyCode: KeyCode.KEY_A,
|
|
75
|
+
altKey: true,
|
|
76
|
+
ctrlKey: true,
|
|
77
|
+
metaKey: true,
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
expect(handler).toBeCalledTimes(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should ignore global events', () => {
|
|
84
|
+
const handler = jest.fn().mockImplementation(() => { });
|
|
85
|
+
renderHook(() => useKeyboard(handler, [KeyCode.KEY_A], { disableGlobalEvent: true }));
|
|
86
|
+
const event = new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_A });
|
|
87
|
+
document.dispatchEvent(event);
|
|
88
|
+
expect(handler).not.toBeCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should respond to different event types', () => {
|
|
92
|
+
const handler = jest.fn().mockImplementation(() => { });
|
|
93
|
+
renderHook(() => useKeyboard(handler, [KeyCode.KEY_A], { event: 'keyup' }));
|
|
94
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_A }));
|
|
95
|
+
expect(handler).not.toBeCalled();
|
|
96
|
+
document.dispatchEvent(new KeyboardEvent('keypress', { keyCode: KeyCode.KEY_A }));
|
|
97
|
+
expect(handler).not.toBeCalled();
|
|
98
|
+
document.dispatchEvent(new KeyboardEvent('keyup', { keyCode: KeyCode.KEY_A }));
|
|
99
|
+
expect(handler).toBeCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should pass the keyboard events', async () => {
|
|
103
|
+
const handler = jest.fn().mockImplementation(() => { });
|
|
104
|
+
const nativeHandler = jest.fn().mockImplementation(() => { });
|
|
105
|
+
const { result } = renderHook<KeyboardResult, {}>(() =>
|
|
106
|
+
useKeyboard(handler, KeyCode.Escape),
|
|
107
|
+
);
|
|
108
|
+
const { container } = render(
|
|
109
|
+
<div onKeyDown={nativeHandler} >
|
|
110
|
+
<span data-testid="inner" {...result.current.bindings} />
|
|
111
|
+
</div>,
|
|
112
|
+
);
|
|
113
|
+
const inner = await screen.findByTestId('inner');
|
|
114
|
+
fireEvent.keyUp(inner, {
|
|
115
|
+
keyCode: KeyCode.Escape,
|
|
116
|
+
}); // { key: 'Enter', code: 'Enter', charCode: 13})
|
|
117
|
+
expect(handler).not.toBeCalled();
|
|
118
|
+
expect(nativeHandler).not.toBeCalled();
|
|
119
|
+
fireEvent.keyDown(inner, {
|
|
120
|
+
keyCode: KeyCode.Escape,
|
|
121
|
+
});
|
|
122
|
+
expect(handler).toBeCalled();
|
|
123
|
+
expect(nativeHandler).toBeCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should prevent default events', async () => {
|
|
127
|
+
const handler = jest.fn().mockImplementation(() => { });
|
|
128
|
+
const nativeHandler = jest.fn().mockImplementation(() => { });
|
|
129
|
+
const { result } = renderHook<KeyboardResult, {}>(() => useKeyboard(handler, KeyCode.Escape, {
|
|
130
|
+
disableGlobalEvent: true,
|
|
131
|
+
stopPropagation: true,
|
|
132
|
+
}));
|
|
133
|
+
const { container } = render(
|
|
134
|
+
<div onKeyDown={nativeHandler} >
|
|
135
|
+
<span data-testid="inner" {...result.current.bindings} />
|
|
136
|
+
</div>,
|
|
137
|
+
);
|
|
138
|
+
const inner = await screen.findByTestId('inner');
|
|
139
|
+
expect(nativeHandler).not.toBeCalled();
|
|
140
|
+
fireEvent.keyDown(inner, {
|
|
141
|
+
keyCode: KeyCode.Escape,
|
|
142
|
+
});
|
|
143
|
+
expect(handler).toBeCalled();
|
|
144
|
+
expect(nativeHandler).not.toBeCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should trigger capture event', async () => {
|
|
148
|
+
const handler = jest.fn().mockImplementation(() => { });
|
|
149
|
+
const { result } = renderHook<KeyboardResult, {}>(() =>
|
|
150
|
+
useKeyboard(handler, KeyCode.Escape, { capture: true, disableGlobalEvent: true }),
|
|
151
|
+
);
|
|
152
|
+
const { container } = render(
|
|
153
|
+
<div onKeyDownCapture={result.current.bindings.onKeyDownCapture} >
|
|
154
|
+
<span data-testid="inner" />
|
|
155
|
+
</div>,
|
|
156
|
+
);
|
|
157
|
+
const inner = await screen.findByTestId('inner');
|
|
158
|
+
fireEvent.keyDown(inner, {
|
|
159
|
+
keyCode: KeyCode.Escape,
|
|
160
|
+
});
|
|
161
|
+
expect(handler).toBeCalled();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
|
|
2
|
+
import React, { useEffect } from 'react';
|
|
3
|
+
import { KeyCode, KeyMod } from './codes';
|
|
4
|
+
import { getActiveModMap, getCtrlKeysByPlatform } from './helper';
|
|
5
|
+
|
|
6
|
+
export const useKeyboard: UseKeyboard = (handler, keyBindings, options = {}) => {
|
|
7
|
+
const bindings = Array.isArray(keyBindings) ? (keyBindings as number[]) : [keyBindings];
|
|
8
|
+
const {
|
|
9
|
+
disableGlobalEvent = false,
|
|
10
|
+
capture = false,
|
|
11
|
+
stopPropagation = false,
|
|
12
|
+
preventDefault = true,
|
|
13
|
+
event = 'keydown',
|
|
14
|
+
} = options;
|
|
15
|
+
const activeModMap = getActiveModMap(bindings);
|
|
16
|
+
const keyCode = bindings.filter((item: number) => !KeyMod[item]);
|
|
17
|
+
const { CtrlCmd, WinCtrl } = getCtrlKeysByPlatform();
|
|
18
|
+
|
|
19
|
+
const eventHandler = (event: React.KeyboardEvent | KeyboardEvent) => {
|
|
20
|
+
if (activeModMap.Shift && !event.shiftKey) return;
|
|
21
|
+
if (activeModMap.Alt && !event.altKey) return;
|
|
22
|
+
if (activeModMap.CtrlCmd && !event[CtrlCmd]) return;
|
|
23
|
+
if (activeModMap.WinCtrl && !event[WinCtrl]) return;
|
|
24
|
+
const hitOne = keyCode.find(k => k === event.keyCode);
|
|
25
|
+
if (keyCode && !hitOne) return;
|
|
26
|
+
if (stopPropagation) {
|
|
27
|
+
event.stopPropagation();
|
|
28
|
+
}
|
|
29
|
+
if (preventDefault) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
}
|
|
32
|
+
handler && handler(event);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!disableGlobalEvent) {
|
|
37
|
+
document.addEventListener(event, eventHandler);
|
|
38
|
+
}
|
|
39
|
+
return () => {
|
|
40
|
+
document.removeEventListener(event, eventHandler);
|
|
41
|
+
};
|
|
42
|
+
}, [disableGlobalEvent]);
|
|
43
|
+
|
|
44
|
+
const elementBindingHandler = (
|
|
45
|
+
elementEventType: 'keydown' | 'keypress' | 'keyup',
|
|
46
|
+
isCapture: boolean = false,
|
|
47
|
+
) => {
|
|
48
|
+
if (elementEventType !== event) return () => { };
|
|
49
|
+
if (isCapture !== capture) return () => { };
|
|
50
|
+
return (e: React.KeyboardEvent) => eventHandler(e);
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
bindings: {
|
|
54
|
+
onKeyDown: elementBindingHandler('keydown'),
|
|
55
|
+
onKeyDownCapture: elementBindingHandler('keydown', true),
|
|
56
|
+
onKeyPress: elementBindingHandler('keypress'),
|
|
57
|
+
onKeyPressCapture: elementBindingHandler('keypress', true),
|
|
58
|
+
onKeyUp: elementBindingHandler('keyup'),
|
|
59
|
+
onKeyUpCapture: elementBindingHandler('keyup', true),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export { KeyMod, KeyCode };
|
|
65
|
+
|
|
66
|
+
export type KeyboardOptions = {
|
|
67
|
+
disableGlobalEvent?: boolean
|
|
68
|
+
stopPropagation?: boolean
|
|
69
|
+
preventDefault?: boolean
|
|
70
|
+
capture?: boolean
|
|
71
|
+
event?: 'keydown' | 'keypress' | 'keyup'
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type KeyboardResult = {
|
|
75
|
+
bindings: {
|
|
76
|
+
onKeyDown: React.KeyboardEventHandler
|
|
77
|
+
onKeyDownCapture: React.KeyboardEventHandler
|
|
78
|
+
onKeyPress: React.KeyboardEventHandler
|
|
79
|
+
onKeyPressCapture: React.KeyboardEventHandler
|
|
80
|
+
onKeyUp: React.KeyboardEventHandler
|
|
81
|
+
onKeyUpCapture: React.KeyboardEventHandler
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type UseKeyboardHandler = (event: React.KeyboardEvent | KeyboardEvent) => void;
|
|
86
|
+
|
|
87
|
+
export type UseKeyboard = (
|
|
88
|
+
handler: UseKeyboardHandler,
|
|
89
|
+
keyBindings: Array<number> | number,
|
|
90
|
+
options?: KeyboardOptions,
|
|
91
|
+
) => KeyboardResult;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { LayoutDefaults } from '@websolutespa/bom-mixer-mock';
|
|
3
|
+
import { LayoutProvider } from '../useLayout/useLayout';
|
|
4
|
+
import { LabelProvider, parseParams, useLabel } from './useLabel';
|
|
5
|
+
|
|
6
|
+
const wrapper = (props: { children: React.ReactNode }) => (
|
|
7
|
+
<LayoutProvider layout={LayoutDefaults}>
|
|
8
|
+
<LabelProvider>{props.children}</LabelProvider>
|
|
9
|
+
</LayoutProvider>
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
describe('useLabel', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should throw without provider', () => {
|
|
18
|
+
expect(() => {
|
|
19
|
+
renderHook(() => useLabel());
|
|
20
|
+
}).toThrow('useGenericContext must be used within a Provider');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should not throw with provider', () => {
|
|
24
|
+
expect(() => {
|
|
25
|
+
renderHook(() => useLabel(), { wrapper });
|
|
26
|
+
}).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should not throw with provider null', () => {
|
|
30
|
+
expect(() => {
|
|
31
|
+
renderHook(() => useLabel(), { wrapper });
|
|
32
|
+
}).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should return formatted value', () => {
|
|
36
|
+
const { result } = renderHook(() => useLabel(), { wrapper });
|
|
37
|
+
const formatted = result.current('error.required');
|
|
38
|
+
expect(formatted).toBe('Required field');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('non existing key should return key', () => {
|
|
42
|
+
const { result } = renderHook(() => useLabel(), { wrapper });
|
|
43
|
+
const formatted = result.current('not.existing');
|
|
44
|
+
expect(formatted).toBe('not.existing');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should return formatted value with params', () => {
|
|
48
|
+
const { result } = renderHook(() => useLabel(), { wrapper });
|
|
49
|
+
const formatted = result.current('items.found', { items: 123 });
|
|
50
|
+
expect(formatted).toBe('123 items found');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('value with params without param should return formatted value with token', () => {
|
|
54
|
+
const { result } = renderHook(() => useLabel(), { wrapper });
|
|
55
|
+
const formatted = result.current('items.found');
|
|
56
|
+
expect(formatted).toBe('@items items found');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('parseParams should format value params', () => {
|
|
60
|
+
const formatted = parseParams('test @param', { param: 1 });
|
|
61
|
+
expect(formatted).toBe('test 1');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('parseParams should format value without params', () => {
|
|
65
|
+
const formatted = parseParams('test @param', {});
|
|
66
|
+
expect(formatted).toBe('test @param');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createGenericContext } from '../useGenericContext/useGenericContext';
|
|
3
|
+
import { useLayout } from '../useLayout/useLayout';
|
|
4
|
+
|
|
5
|
+
export type ILabelContext = (key: string, params?: { [key: string]: any }) => string;
|
|
6
|
+
|
|
7
|
+
const [useLabel, LabelContextProvider] = createGenericContext<ILabelContext>();
|
|
8
|
+
|
|
9
|
+
function LabelProvider({ children }: { children?: React.ReactNode }) {
|
|
10
|
+
|
|
11
|
+
// const { locale, labels } = useLayout() || { locale: '*', labels: [] };
|
|
12
|
+
const { locale, labels } = useLayout();
|
|
13
|
+
|
|
14
|
+
// console.log('LabelProvider', locale);
|
|
15
|
+
|
|
16
|
+
const dictionary = Object.fromEntries(labels.map(l => [l.id, l.text]));
|
|
17
|
+
|
|
18
|
+
const context = (key: string, params?: { [key: string]: any }): string => {
|
|
19
|
+
const label = (dictionary[key] != null ? dictionary[key] : key) as string;
|
|
20
|
+
if (params) {
|
|
21
|
+
return parseParams(label, params);
|
|
22
|
+
}
|
|
23
|
+
return label;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// console.log('LabelProvider.context', context);
|
|
27
|
+
return (
|
|
28
|
+
<LabelContextProvider value={context}>
|
|
29
|
+
{children}
|
|
30
|
+
</LabelContextProvider>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseParams(label: string, params: { [key: string]: any }): string {
|
|
35
|
+
const TEMPLATE_REGEXP: RegExp = /@(\w+)/g;
|
|
36
|
+
return label.replace(TEMPLATE_REGEXP, (text: string, key: string) => {
|
|
37
|
+
const replacer: unknown = params[key];
|
|
38
|
+
return typeof replacer !== 'undefined' ? String(replacer) : text;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { useLabel, LabelProvider, parseParams };
|
|
43
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// readme https://testing-library.com/
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { LayoutDefaults } from '@websolutespa/bom-mixer-mock';
|
|
4
|
+
import { LayoutProvider, useLayout } from './useLayout';
|
|
5
|
+
|
|
6
|
+
const wrapper = (props: { children: React.ReactNode }) => (
|
|
7
|
+
<LayoutProvider layout={LayoutDefaults}>{props.children}</LayoutProvider>
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
describe('useLayout', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('should throw without provider', () => {
|
|
16
|
+
expect(() => {
|
|
17
|
+
renderHook(() => useLayout());
|
|
18
|
+
}).toThrow('useGenericContext must be used within a Provider');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should not throw with provider', () => {
|
|
22
|
+
expect(() => {
|
|
23
|
+
renderHook(() => useLayout(), { wrapper });
|
|
24
|
+
}).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should return layout', () => {
|
|
28
|
+
const { result } = renderHook(() => useLayout(), { wrapper });
|
|
29
|
+
expect(result.current).toBe(LayoutDefaults);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ILayout } from '@websolutespa/bom-core';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { createGenericContext } from '../useGenericContext/useGenericContext';
|
|
4
|
+
|
|
5
|
+
export type ILayoutContext = ILayout;
|
|
6
|
+
|
|
7
|
+
const [useLayout, LayoutContextProvider] = createGenericContext<ILayoutContext>();
|
|
8
|
+
|
|
9
|
+
function LayoutProvider({ children, layout }: { children?: React.ReactNode, layout: ILayout }) {
|
|
10
|
+
// console.log('LayoutProvider.context', context);
|
|
11
|
+
return (
|
|
12
|
+
<LayoutContextProvider value={layout}>
|
|
13
|
+
{children}
|
|
14
|
+
</LayoutContextProvider>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { useLayout, LayoutProvider };
|
|
19
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// readme https://testing-library.com/
|
|
4
|
+
import { act, renderHook } from '@testing-library/react';
|
|
5
|
+
import { useModal } from './useModal';
|
|
6
|
+
|
|
7
|
+
describe('useModal', () => {
|
|
8
|
+
it('should work correctly', async () => {
|
|
9
|
+
const { result } = renderHook(() => useModal());
|
|
10
|
+
let [modal, openModal, closeModal] = result.current;
|
|
11
|
+
expect(modal).toBe(undefined);
|
|
12
|
+
act(() => openModal('test'));
|
|
13
|
+
[modal, openModal, closeModal] = result.current;
|
|
14
|
+
expect(modal).toBe('test');
|
|
15
|
+
act(() => closeModal());
|
|
16
|
+
[modal, openModal, closeModal] = result.current;
|
|
17
|
+
expect(modal).toBe(undefined);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { UIValue, useUI } from '../useUI/useUI';
|
|
2
|
+
|
|
3
|
+
export function useModal(): [modal: UIValue, open: (value?: string) => void, close: () => void] {
|
|
4
|
+
const modal = useUI(state => state.views.modal);
|
|
5
|
+
const reduce = useUI(state => state.actions.reduce);
|
|
6
|
+
function setModal(value?: string | number) {
|
|
7
|
+
reduce('modal', (views) => value);
|
|
8
|
+
}
|
|
9
|
+
function openModal(value?: string | number) {
|
|
10
|
+
return setModal(value);
|
|
11
|
+
}
|
|
12
|
+
function closeModal() {
|
|
13
|
+
return setModal();
|
|
14
|
+
}
|
|
15
|
+
return [modal, openModal, closeModal];
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { useMounted } from './useMounted';
|
|
4
|
+
|
|
5
|
+
describe('useMounted', () => {
|
|
6
|
+
|
|
7
|
+
it('should be defined', () => {
|
|
8
|
+
expect(useMounted).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return a boolean', () => {
|
|
12
|
+
const { result } = renderHook(() => useMounted(), { initialProps: false });
|
|
13
|
+
expect(result.current).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
it('should return a function', () => {
|
|
18
|
+
const { result } = renderHook(() => useMounted(), { initialProps: false });
|
|
19
|
+
expect(typeof result.current).toEqual('function');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return true if component is mounted', () => {
|
|
23
|
+
const { result } = renderHook(() => useMounted(), { initialProps: false });
|
|
24
|
+
expect(result.current()).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return false if component is unmounted', () => {
|
|
28
|
+
const { result, unmount } = renderHook(() => useMounted(), { initialProps: false });
|
|
29
|
+
unmount();
|
|
30
|
+
expect(result.current()).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
export function useMounted(): () => boolean {
|
|
5
|
+
const ref = useRef<boolean>(false);
|
|
6
|
+
const get = useCallback(() => ref.current, []);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
ref.current = true;
|
|
9
|
+
return () => {
|
|
10
|
+
ref.current = false;
|
|
11
|
+
};
|
|
12
|
+
}, []);
|
|
13
|
+
return get;
|
|
14
|
+
}
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
18
|
+
|
|
19
|
+
export function useMounted() {
|
|
20
|
+
const [mounted, setMounted] = useState(false);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setMounted(true);
|
|
23
|
+
}, []); // at init only
|
|
24
|
+
return mounted;
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { LayoutDefaults } from '@websolutespa/bom-mixer-mock';
|
|
3
|
+
import { LayoutProvider } from '../useLayout/useLayout';
|
|
4
|
+
import { useNumber } from './useNumber';
|
|
5
|
+
|
|
6
|
+
const wrapper = (props: { children: React.ReactNode }) => (
|
|
7
|
+
<LayoutProvider layout={LayoutDefaults}>{props.children}</LayoutProvider>
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
describe('useNumber', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('should throw without provider', () => {
|
|
16
|
+
expect(() => {
|
|
17
|
+
renderHook(() => useNumber());
|
|
18
|
+
}).toThrow('useGenericContext must be used within a Provider');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should not throw with provider', () => {
|
|
22
|
+
expect(() => {
|
|
23
|
+
renderHook(() => useNumber(), { wrapper });
|
|
24
|
+
}).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should return formatted value', () => {
|
|
28
|
+
const { result } = renderHook(() => useNumber({
|
|
29
|
+
minimumFractionDigits: 2,
|
|
30
|
+
maximumFractionDigits: 2
|
|
31
|
+
}), { wrapper });
|
|
32
|
+
const formatted = result.current(1234.5678);
|
|
33
|
+
expect(formatted).toEqual('1,234.57');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useLayout } from '../useLayout/useLayout';
|
|
3
|
+
|
|
4
|
+
export function useNumber(options?: Intl.NumberFormatOptions, locale?: string): (value: number) => string {
|
|
5
|
+
const layout = useLayout();
|
|
6
|
+
const currentLocale = locale || layout.locale;
|
|
7
|
+
const flatOptions = options ? Object.entries(options).map(([k, v]) => `${k}:${v}`).join('') : undefined;
|
|
8
|
+
const formatter = useMemo(() => {
|
|
9
|
+
const formatter = new Intl.NumberFormat(currentLocale, options).format;
|
|
10
|
+
return formatter;
|
|
11
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
12
|
+
}, [currentLocale, flatOptions]);
|
|
13
|
+
|
|
14
|
+
return formatter;
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { PageDefaults } from '@websolutespa/bom-mixer-mock';
|
|
3
|
+
import { PageProvider, usePage } from './usePage';
|
|
4
|
+
|
|
5
|
+
const wrapper = (props: { children: React.ReactNode }) => (
|
|
6
|
+
<PageProvider page={PageDefaults}>{props.children}</PageProvider>
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
describe('usePage', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should throw without provider', () => {
|
|
15
|
+
expect(() => {
|
|
16
|
+
renderHook(() => usePage());
|
|
17
|
+
}).toThrow('useGenericContext must be used within a Provider');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should not throw with provider', () => {
|
|
21
|
+
expect(() => {
|
|
22
|
+
renderHook(() => usePage(), { wrapper });
|
|
23
|
+
}).not.toThrow();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should return page', () => {
|
|
27
|
+
const { result } = renderHook(() => usePage(), { wrapper });
|
|
28
|
+
expect(result.current).toBe(PageDefaults);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { IPage } from '@websolutespa/bom-core';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { createGenericContext } from '../useGenericContext/useGenericContext';
|
|
4
|
+
|
|
5
|
+
export type IPageContext = IPage;
|
|
6
|
+
|
|
7
|
+
const [usePage, PageContextProvider] = createGenericContext<IPageContext>();
|
|
8
|
+
|
|
9
|
+
function PageProvider({ children, page }: { children?: React.ReactNode, page: IPage }) {
|
|
10
|
+
// console.log('PageProvider.context', context);
|
|
11
|
+
return (
|
|
12
|
+
<PageContextProvider value={page}>
|
|
13
|
+
{children}
|
|
14
|
+
</PageContextProvider>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { usePage, PageProvider };
|
|
19
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type IPaginationInfo<T> = {
|
|
2
|
+
items: T[],
|
|
3
|
+
total: number,
|
|
4
|
+
pages: number,
|
|
5
|
+
page: number,
|
|
6
|
+
perPage: number,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function getPagedItems<T>(items: T[], page: number, perPage: number): T[] {
|
|
10
|
+
const from = (page - 1) * perPage;
|
|
11
|
+
const to = Math.min(page * perPage, items.length);
|
|
12
|
+
return items.slice(from, to);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
export function getPaginationInfo<T>(items: T[], page: number = 1, perPage: number = 15): IPaginationInfo<T> {
|
|
17
|
+
const total = items.length;
|
|
18
|
+
const pages = Math.ceil(total / perPage);
|
|
19
|
+
const pagedItems = getPagedItems(items, page, perPage);
|
|
20
|
+
// console.log(items.length, '->', pagedItems.length);
|
|
21
|
+
return { items: pagedItems, total, pages, page, perPage };
|
|
22
|
+
}
|
|
23
|
+
*/
|