@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,89 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// readme https://testing-library.com/
|
|
4
|
+
import { act, renderHook } from '@testing-library/react';
|
|
5
|
+
import { CartProvider, useCart } from './useCart';
|
|
6
|
+
|
|
7
|
+
const wrapper = (props: { children: React.ReactNode }) => (
|
|
8
|
+
<CartProvider>{props.children}</CartProvider>
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const item = {
|
|
12
|
+
id: 1,
|
|
13
|
+
price: 100.0,
|
|
14
|
+
title: 'Item',
|
|
15
|
+
href: '#',
|
|
16
|
+
media: {
|
|
17
|
+
src: '/path/to/image.png',
|
|
18
|
+
type: 'image',
|
|
19
|
+
},
|
|
20
|
+
schema: 'product',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('useCart', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should throw without provider', () => {
|
|
29
|
+
expect(() => {
|
|
30
|
+
renderHook(() => useCart(state => state));
|
|
31
|
+
}).toThrow('Missing CartContext.Provider in the tree');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should not throw with provider', () => {
|
|
35
|
+
expect(() => {
|
|
36
|
+
renderHook(() => useCart(state => state), { wrapper });
|
|
37
|
+
}).not.toThrow();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should work correctly', async () => {
|
|
41
|
+
const { result } = renderHook(() => useCart(state => state), { wrapper });
|
|
42
|
+
let state = result.current;
|
|
43
|
+
// read
|
|
44
|
+
expect(state.items).toStrictEqual([]);
|
|
45
|
+
expect(state.actions.count()).toBe(0);
|
|
46
|
+
expect(state.actions.has(item)).toBe(false);
|
|
47
|
+
// add
|
|
48
|
+
act(() => state.actions.add(item, 1));
|
|
49
|
+
state = result.current;
|
|
50
|
+
expect(state.actions.count()).toBe(1);
|
|
51
|
+
expect(state.actions.has(item)).toBe(true);
|
|
52
|
+
state = result.current;
|
|
53
|
+
expect(state.items).toStrictEqual([{ ...item, qty: 1 }]);
|
|
54
|
+
const cartItem = state.actions.find(item);
|
|
55
|
+
expect(state.items[0]).toBe(cartItem);
|
|
56
|
+
// re add
|
|
57
|
+
act(() => state.actions.add(item, 1));
|
|
58
|
+
state = result.current;
|
|
59
|
+
expect(state.items).toStrictEqual([{ ...item, qty: 1 }]);
|
|
60
|
+
// rehydrate
|
|
61
|
+
const rehydrate = renderHook(() => useCart(state => state), { wrapper });
|
|
62
|
+
expect(rehydrate.result.current.items).toStrictEqual([{ ...item, qty: 1 }]);
|
|
63
|
+
// update
|
|
64
|
+
act(() => state.actions.update({ ...item, qty: 2 }));
|
|
65
|
+
state = result.current;
|
|
66
|
+
expect(state.items).toStrictEqual([{ ...item, qty: 2 }]);
|
|
67
|
+
// update non exhistent
|
|
68
|
+
act(() => state.actions.update({ ...item, id: 2, qty: 1 }));
|
|
69
|
+
state = result.current;
|
|
70
|
+
expect(state.items).toStrictEqual([{ ...item, qty: 2 }]);
|
|
71
|
+
// remove
|
|
72
|
+
act(() => state.actions.remove(item));
|
|
73
|
+
state = result.current;
|
|
74
|
+
expect(state.items).toStrictEqual([]);
|
|
75
|
+
expect(state.actions.count()).toBe(0);
|
|
76
|
+
// remove non exhistent
|
|
77
|
+
act(() => state.actions.remove({ ...item, id: 2 }));
|
|
78
|
+
state = result.current;
|
|
79
|
+
expect(state.actions.count()).toBe(0);
|
|
80
|
+
// clear
|
|
81
|
+
act(() => state.actions.add(item));
|
|
82
|
+
state = result.current;
|
|
83
|
+
act(() => state.actions.clear());
|
|
84
|
+
state = result.current;
|
|
85
|
+
expect(state.items).toStrictEqual([]);
|
|
86
|
+
expect(state.actions.count()).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { ISchema } from '@websolutespa/bom-core';
|
|
2
|
+
import { ICartAddItem, ICartItem } from '@websolutespa/bom-mixer-models';
|
|
3
|
+
import { createContext, useContext, useRef } from 'react';
|
|
4
|
+
import { createStore, useStore } from 'zustand';
|
|
5
|
+
import { StateStorage, createJSONStorage, persist } from 'zustand/middleware';
|
|
6
|
+
|
|
7
|
+
type CartProps = {
|
|
8
|
+
hydrated: boolean;
|
|
9
|
+
items: ICartItem[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type CartActions = {
|
|
13
|
+
count(): number;
|
|
14
|
+
has(item: ISchema): boolean;
|
|
15
|
+
find(item: ISchema): ICartItem | undefined;
|
|
16
|
+
add(item: ICartAddItem, qty?: number): void;
|
|
17
|
+
remove(item: ISchema): number;
|
|
18
|
+
update(item: ICartItem): void;
|
|
19
|
+
clear(): void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type CartState = CartProps & {
|
|
23
|
+
actions: CartActions;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type CartStore = ReturnType<typeof createCartStore>;
|
|
27
|
+
|
|
28
|
+
const createCartStore = ({ storage, ...initialProps }: { storage?: StateStorage } & Partial<CartProps>) => {
|
|
29
|
+
const DEFAULT_PROPS: CartProps = {
|
|
30
|
+
hydrated: false,
|
|
31
|
+
items: [],
|
|
32
|
+
};
|
|
33
|
+
const useStore = createStore<CartState>()(
|
|
34
|
+
persist(
|
|
35
|
+
(set, get) => ({
|
|
36
|
+
...DEFAULT_PROPS,
|
|
37
|
+
...initialProps,
|
|
38
|
+
actions: {
|
|
39
|
+
count: () => {
|
|
40
|
+
return get().items.length;
|
|
41
|
+
},
|
|
42
|
+
has: (item: ISchema) => {
|
|
43
|
+
return get().items.find(x => x.schema === item.schema && x.id === item.id) != null;
|
|
44
|
+
},
|
|
45
|
+
find: (item: ISchema) => {
|
|
46
|
+
return get().items.find(x => x.schema === item.schema && x.id === item.id);
|
|
47
|
+
},
|
|
48
|
+
add: (item: ICartItem, qty: number = 1) => set(state => {
|
|
49
|
+
const items = state.items.slice();
|
|
50
|
+
const index = items.reduce((p, c, i) => (c.schema === item.schema && c.id === item.id) ? i : p, -1);
|
|
51
|
+
if (index === -1) {
|
|
52
|
+
items.push({ ...item, qty });
|
|
53
|
+
}
|
|
54
|
+
return { items };
|
|
55
|
+
}),
|
|
56
|
+
remove: (item: ISchema) => {
|
|
57
|
+
let count = 0;
|
|
58
|
+
set(state => {
|
|
59
|
+
const items = state.items.slice();
|
|
60
|
+
const index = items.reduce((p, c, i) => (c.schema === item.schema && c.id === item.id) ? i : p, -1);
|
|
61
|
+
if (index !== -1) {
|
|
62
|
+
items.splice(index, 1);
|
|
63
|
+
}
|
|
64
|
+
count = items.length;
|
|
65
|
+
return { items };
|
|
66
|
+
});
|
|
67
|
+
return count;
|
|
68
|
+
},
|
|
69
|
+
update: (item: ICartItem) => set(state => {
|
|
70
|
+
const items = state.items.slice();
|
|
71
|
+
const index = items.reduce((p, c, i) => (c.schema === item.schema && c.id === item.id) ? i : p, -1);
|
|
72
|
+
if (index !== -1) {
|
|
73
|
+
items[index] = item;
|
|
74
|
+
}
|
|
75
|
+
return { items };
|
|
76
|
+
}),
|
|
77
|
+
clear: () => set(state => ({ items: [] })),
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
{
|
|
81
|
+
name: 'cart',
|
|
82
|
+
storage: createJSONStorage(() => storage || localStorage),
|
|
83
|
+
/*
|
|
84
|
+
getStorage: () => {
|
|
85
|
+
// console.log('useCart.getStorage');
|
|
86
|
+
return storage || localStorage;
|
|
87
|
+
},
|
|
88
|
+
*/
|
|
89
|
+
onRehydrateStorage: () => () => {
|
|
90
|
+
useStore.setState({ hydrated: true });
|
|
91
|
+
// console.log('onRehydrateStorage', useStore, useStore.getState());
|
|
92
|
+
},
|
|
93
|
+
merge: (persistedState: any, currentState: CartState) => {
|
|
94
|
+
if (persistedState.items && Array.isArray(persistedState.items)) {
|
|
95
|
+
currentState.items = [...persistedState.items];
|
|
96
|
+
}
|
|
97
|
+
currentState.hydrated = true;
|
|
98
|
+
return currentState;
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
return useStore;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const CartContext = createContext<CartStore | null>(null);
|
|
107
|
+
|
|
108
|
+
type CartProviderProps = React.PropsWithChildren<{ items?: ICartItem[], storage?: StateStorage }>;
|
|
109
|
+
|
|
110
|
+
function CartProvider({ children, ...props }: CartProviderProps) {
|
|
111
|
+
const storeRef = useRef<CartStore>();
|
|
112
|
+
if (!storeRef.current) {
|
|
113
|
+
storeRef.current = createCartStore(props);
|
|
114
|
+
}
|
|
115
|
+
return (
|
|
116
|
+
<CartContext.Provider value={storeRef.current}>
|
|
117
|
+
{children}
|
|
118
|
+
</CartContext.Provider>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function useCart<T>(
|
|
123
|
+
selector: (state: CartState) => T,
|
|
124
|
+
equalityFn?: (left: T, right: T) => boolean
|
|
125
|
+
): T {
|
|
126
|
+
const store = useContext(CartContext);
|
|
127
|
+
if (!store) throw new Error('Missing CartContext.Provider in the tree');
|
|
128
|
+
return useStore(store, selector, equalityFn);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { CartProvider, useCart };
|
|
132
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// readme https://testing-library.com/
|
|
4
|
+
import { act, renderHook } from '@testing-library/react';
|
|
5
|
+
import { ICheckoutPartial } from '@websolutespa/bom-mixer-models';
|
|
6
|
+
import { CheckoutProvider, useCheckout } from './useCheckout';
|
|
7
|
+
|
|
8
|
+
const data = {
|
|
9
|
+
countries: [],
|
|
10
|
+
regions: [],
|
|
11
|
+
provinces: [],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const wrapper = (props: { children: React.ReactNode }) => (
|
|
15
|
+
<CheckoutProvider data={data}>{props.children}</CheckoutProvider>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const checkout: ICheckoutPartial = {
|
|
19
|
+
items: [],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('useCheckout', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should throw without provider', () => {
|
|
28
|
+
expect(() => {
|
|
29
|
+
renderHook(() => useCheckout(state => state));
|
|
30
|
+
}).toThrow('Missing CheckoutContext.Provider in the tree');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should not throw with provider', () => {
|
|
34
|
+
expect(() => {
|
|
35
|
+
renderHook(() => useCheckout(state => state), { wrapper });
|
|
36
|
+
}).not.toThrow();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should work correctly', async () => {
|
|
40
|
+
const { result } = renderHook(() => useCheckout(state => state), { wrapper });
|
|
41
|
+
let state = result.current;
|
|
42
|
+
// read
|
|
43
|
+
expect(state.checkout).toStrictEqual({});
|
|
44
|
+
// set
|
|
45
|
+
act(() => state.actions.setCheckout(checkout));
|
|
46
|
+
state = result.current;
|
|
47
|
+
expect(state.checkout).toStrictEqual(checkout);
|
|
48
|
+
// rehydrate
|
|
49
|
+
const rehydrate = renderHook(() => useCheckout(state => state), { wrapper });
|
|
50
|
+
expect(rehydrate.result.current.checkout).toStrictEqual(checkout);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { IAddressOptions, ICheckoutPartial } from '@websolutespa/bom-mixer-models';
|
|
2
|
+
import { createContext, useContext, useRef } from 'react';
|
|
3
|
+
import { createStore, useStore } from 'zustand';
|
|
4
|
+
import { createJSONStorage, persist, StateStorage } from 'zustand/middleware';
|
|
5
|
+
|
|
6
|
+
type CheckoutInitialProps = {
|
|
7
|
+
data: IAddressOptions;
|
|
8
|
+
checkout?: ICheckoutPartial;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type CheckoutProps = {
|
|
12
|
+
hydrated: boolean;
|
|
13
|
+
data: IAddressOptions;
|
|
14
|
+
checkout: ICheckoutPartial;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type CheckoutActions = {
|
|
18
|
+
setCheckout(checkout: ICheckoutPartial): void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CheckoutState = CheckoutProps & {
|
|
22
|
+
actions: CheckoutActions,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type CheckoutStore = ReturnType<typeof createCheckoutStore>;
|
|
26
|
+
|
|
27
|
+
const createCheckoutStore = ({ storage, ...initialProps }: { storage?: StateStorage } & CheckoutInitialProps) => {
|
|
28
|
+
const DEFAULT_PROPS = {
|
|
29
|
+
hydrated: false,
|
|
30
|
+
checkout: {},
|
|
31
|
+
};
|
|
32
|
+
const useStore = createStore<CheckoutState>()(
|
|
33
|
+
persist(
|
|
34
|
+
(set, get) => ({
|
|
35
|
+
...DEFAULT_PROPS,
|
|
36
|
+
...initialProps,
|
|
37
|
+
actions: {
|
|
38
|
+
setCheckout: (checkout: ICheckoutPartial) => set(state => {
|
|
39
|
+
return { checkout };
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
{
|
|
44
|
+
name: 'checkout',
|
|
45
|
+
storage: createJSONStorage(() => storage || localStorage),
|
|
46
|
+
/*
|
|
47
|
+
getStorage: () => {
|
|
48
|
+
// console.log('useCheckout.getStorage');
|
|
49
|
+
return storage || localStorage;
|
|
50
|
+
},
|
|
51
|
+
*/
|
|
52
|
+
onRehydrateStorage: () => () => {
|
|
53
|
+
useStore.setState({ hydrated: true });
|
|
54
|
+
// console.log('onRehydrateStorage', useStore, useStore.getState());
|
|
55
|
+
},
|
|
56
|
+
merge: (persistedState: any, currentState: CheckoutState) => {
|
|
57
|
+
if (persistedState.checkout && typeof persistedState.checkout === 'object') {
|
|
58
|
+
currentState.checkout = { ...persistedState.checkout };
|
|
59
|
+
}
|
|
60
|
+
currentState.hydrated = true;
|
|
61
|
+
return currentState;
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
return useStore;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const CheckoutContext = createContext<CheckoutStore | null>(null);
|
|
70
|
+
|
|
71
|
+
type CheckoutProviderProps = React.PropsWithChildren<{ data: IAddressOptions, checkout?: ICheckoutPartial, storage?: StateStorage }>;
|
|
72
|
+
|
|
73
|
+
function CheckoutProvider({ children, ...props }: CheckoutProviderProps) {
|
|
74
|
+
const storeRef = useRef<CheckoutStore>();
|
|
75
|
+
if (!storeRef.current) {
|
|
76
|
+
storeRef.current = createCheckoutStore(props);
|
|
77
|
+
}
|
|
78
|
+
return (
|
|
79
|
+
<CheckoutContext.Provider value={storeRef.current}>
|
|
80
|
+
{children}
|
|
81
|
+
</CheckoutContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function useCheckout<T>(
|
|
86
|
+
selector: (state: CheckoutState) => T,
|
|
87
|
+
equalityFn?: (left: T, right: T) => boolean
|
|
88
|
+
): T {
|
|
89
|
+
const store = useContext(CheckoutContext);
|
|
90
|
+
if (!store) throw new Error('Missing CheckoutContext.Provider in the tree');
|
|
91
|
+
return useStore(store, selector, equalityFn);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { CheckoutProvider, useCheckout };
|
|
95
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// readme https://testing-library.com/
|
|
2
|
+
import { fireEvent, renderHook } from '@testing-library/react';
|
|
3
|
+
import { useClickAnyWhere } from './useClickAnyWhere';
|
|
4
|
+
|
|
5
|
+
describe('useClickAnyWhere', () => {
|
|
6
|
+
|
|
7
|
+
it('should work correctly', () => {
|
|
8
|
+
const handler = jest.fn();
|
|
9
|
+
renderHook(() => useClickAnyWhere(handler));
|
|
10
|
+
expect(handler).toHaveBeenCalledTimes(0);
|
|
11
|
+
fireEvent.click(document);
|
|
12
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useClickAnyWhere(handler: (event: MouseEvent) => void) {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const callback = (event: MouseEvent) => handler(event);
|
|
6
|
+
document.addEventListener('click', callback);
|
|
7
|
+
return () => document.removeEventListener('click', callback);
|
|
8
|
+
}, [handler]);
|
|
9
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import React, { useRef } from 'react';
|
|
4
|
+
import { useClickOut } from './useClickOut';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
handler: () => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const Wrapper: React.FunctionComponent<Props> = ({ handler }) => {
|
|
11
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
12
|
+
useClickOut(ref, handler);
|
|
13
|
+
return <div data-testid="inside" ref={ref} />;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('useClickOut', () => {
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
jest.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should call handler when clicked outside', async () => {
|
|
22
|
+
const handler = jest.fn();
|
|
23
|
+
render(
|
|
24
|
+
<>
|
|
25
|
+
<Wrapper handler={handler} />
|
|
26
|
+
<div data-testid="outside" />
|
|
27
|
+
</>
|
|
28
|
+
);
|
|
29
|
+
const outside = screen.getByTestId('outside');
|
|
30
|
+
expect(handler).toHaveBeenCalledTimes(0);
|
|
31
|
+
await userEvent.click(outside);
|
|
32
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should not call handler when clicked inside', async () => {
|
|
36
|
+
const handler = jest.fn();
|
|
37
|
+
render(
|
|
38
|
+
<>
|
|
39
|
+
<Wrapper handler={handler} />
|
|
40
|
+
<div data-testid="outside" />
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
const inside = screen.getByTestId('inside');
|
|
44
|
+
expect(handler).toHaveBeenCalledTimes(0);
|
|
45
|
+
await userEvent.click(inside);
|
|
46
|
+
expect(handler).toHaveBeenCalledTimes(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
it('should call handler when clicked outside', async () => {
|
|
51
|
+
const handler = jest.fn();
|
|
52
|
+
|
|
53
|
+
render(
|
|
54
|
+
<>
|
|
55
|
+
<Wrapper handler={handler} />
|
|
56
|
+
<div data-testid="outside" />
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const inside = screen.getByTestId('inside');
|
|
61
|
+
const outside = screen.getByTestId('outside');
|
|
62
|
+
|
|
63
|
+
expect(handler).toHaveBeenCalledTimes(0);
|
|
64
|
+
|
|
65
|
+
await userEvent.click(inside);
|
|
66
|
+
expect(handler).toHaveBeenCalledTimes(0);
|
|
67
|
+
|
|
68
|
+
await userEvent.click(outside);
|
|
69
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
70
|
+
|
|
71
|
+
await userEvent.click(outside);
|
|
72
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
73
|
+
|
|
74
|
+
await userEvent.click(inside);
|
|
75
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
76
|
+
});
|
|
77
|
+
*/
|
|
78
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MutableRefObject, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ClickOutGetContainer = () => HTMLElement | null;
|
|
4
|
+
|
|
5
|
+
export function useClickOut(ref: MutableRefObject<HTMLElement | null>, handler: (event: MouseEvent) => void) {
|
|
6
|
+
|
|
7
|
+
const handlerRef = useRef(handler);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
handlerRef.current = handler;
|
|
11
|
+
}, [handler]);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const callback = (event: MouseEvent) => {
|
|
15
|
+
const el = ref.current;
|
|
16
|
+
if (!event || !el || el.contains(event.target as Node)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
handlerRef.current(event);
|
|
20
|
+
};
|
|
21
|
+
document.addEventListener('click', callback);
|
|
22
|
+
return () => document.removeEventListener('click', callback);
|
|
23
|
+
}, [ref]);
|
|
24
|
+
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { LayoutDefaults } from '@websolutespa/bom-mixer-mock';
|
|
3
|
+
import { LayoutProvider } from '../useLayout/useLayout';
|
|
4
|
+
import { useCurrency } from './useCurrency';
|
|
5
|
+
|
|
6
|
+
const wrapper = (props: { children: React.ReactNode }) => (
|
|
7
|
+
<LayoutProvider layout={LayoutDefaults}>{props.children}</LayoutProvider>
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
describe('useCurrency', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('should throw without provider', () => {
|
|
16
|
+
expect(() => {
|
|
17
|
+
renderHook(() => useCurrency());
|
|
18
|
+
}).toThrow('useGenericContext must be used within a Provider');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should not throw with provider', () => {
|
|
22
|
+
expect(() => {
|
|
23
|
+
renderHook(() => useCurrency(), { wrapper });
|
|
24
|
+
}).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should return formatted value', () => {
|
|
28
|
+
const { result } = renderHook(() => useCurrency('EUR'), { wrapper });
|
|
29
|
+
const formatted = result.current(1234.5678);
|
|
30
|
+
expect(formatted).toEqual('€1,234.57');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useNumber } from '../useNumber/useNumber';
|
|
2
|
+
|
|
3
|
+
export function useCurrency(currency: string = 'EUR', locale?: string): (value: number) => string {
|
|
4
|
+
return useNumber({ style: 'currency', currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }, locale);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
import { useMemo } from 'react';
|
|
9
|
+
import { useNumber } from 'src/useNumber/useNumber';
|
|
10
|
+
import { useLayout } from '../useLayout/useLayout';
|
|
11
|
+
|
|
12
|
+
export function useCurrency(currency: string = 'EUR', locale?: string): (value: number) => string {
|
|
13
|
+
const layout = useLayout();
|
|
14
|
+
const currentLocale = locale || layout.locale;
|
|
15
|
+
|
|
16
|
+
const formatter = useMemo(() => {
|
|
17
|
+
const options = { style: 'currency', currency };
|
|
18
|
+
const formatter = new Intl.NumberFormat(currentLocale, options).format;
|
|
19
|
+
return formatter;
|
|
20
|
+
}, [currency, currentLocale]);
|
|
21
|
+
|
|
22
|
+
return formatter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
|
|
29
|
+
export function useCurrency(value: number, currency: string = 'EUR', locale?: string): string {
|
|
30
|
+
|
|
31
|
+
const layout = useLayout();
|
|
32
|
+
|
|
33
|
+
const currentLocale = locale || layout.locale;
|
|
34
|
+
|
|
35
|
+
return useMemo(() => {
|
|
36
|
+
const options = { style: 'currency', currency };
|
|
37
|
+
const formattedValue = new Intl.NumberFormat(currentLocale, options).format(value);
|
|
38
|
+
return formattedValue;
|
|
39
|
+
}, [value, currency, currentLocale]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
*/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { act, render, renderHook } from '@testing-library/react';
|
|
2
|
+
import React, { useEffect } from 'react';
|
|
3
|
+
import { useCurrentState } from './useCurrentState';
|
|
4
|
+
|
|
5
|
+
describe('useCurrentState', () => {
|
|
6
|
+
it('should work correctly', () => {
|
|
7
|
+
const { result } = renderHook(() => useCurrentState(''));
|
|
8
|
+
let [state, setState, ref] = result.current;
|
|
9
|
+
expect(state).toEqual('');
|
|
10
|
+
act(() => setState('test'));
|
|
11
|
+
[state, setState, ref] = result.current;
|
|
12
|
+
expect(state).toEqual('test');
|
|
13
|
+
expect(ref.current).toEqual('test');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('functional initial mode should be supported', () => {
|
|
17
|
+
const { result } = renderHook(() => useCurrentState(() => 1 + 1));
|
|
18
|
+
let [state, setState, ref] = result.current;
|
|
19
|
+
expect(state).toEqual(2);
|
|
20
|
+
act(() => setState(0));
|
|
21
|
+
[state, setState, ref] = result.current;
|
|
22
|
+
expect(state).toEqual(0);
|
|
23
|
+
expect(ref.current).toEqual(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('functional update mode should be supported', () => {
|
|
27
|
+
const { result } = renderHook(() => useCurrentState(0));
|
|
28
|
+
let [state, setState, ref] = result.current;
|
|
29
|
+
expect(state).toEqual(0);
|
|
30
|
+
act(() => setState(pre => pre + 10));
|
|
31
|
+
[state, setState, ref] = result.current;
|
|
32
|
+
expect(state).toEqual(10);
|
|
33
|
+
expect(ref.current).toEqual(10);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('only ref should track latest value', () => {
|
|
37
|
+
const Mock: React.FC<unknown> = () => {
|
|
38
|
+
const [state, setState, ref] = useCurrentState('');
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
return () => {
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
expect(state).not.toEqual('test2');
|
|
43
|
+
expect(ref.current).toEqual('test2');
|
|
44
|
+
}, 0);
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setState('test');
|
|
49
|
+
setState('test2');
|
|
50
|
+
}, []);
|
|
51
|
+
return <span />;
|
|
52
|
+
};
|
|
53
|
+
const { container, unmount } = render(<Mock />);
|
|
54
|
+
expect(() => unmount()).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Dispatch, MutableRefObject, SetStateAction, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useCurrentState<S>(initialState: S | (() => S)): CurrentStateType<S> {
|
|
4
|
+
const [state, setState] = useState<S>(() => {
|
|
5
|
+
return typeof initialState === 'function' ? (initialState as () => S)() : initialState;
|
|
6
|
+
});
|
|
7
|
+
const ref = useRef<S>(initialState as S);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
ref.current = state;
|
|
11
|
+
}, [state]);
|
|
12
|
+
|
|
13
|
+
const setValue = (val: SetStateAction<S>) => {
|
|
14
|
+
const result = typeof val === 'function' ? (val as (prevState: S) => S)(ref.current) : val;
|
|
15
|
+
ref.current = result;
|
|
16
|
+
setState(result);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return [state, setValue, ref];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type CurrentStateType<S> = [S, Dispatch<SetStateAction<S>>, MutableRefObject<S>];
|