@websolutespa/bom-mixer-hooks 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.mjs +1 -1
  4. package/package.json +7 -5
  5. package/src/index.ts +60 -0
  6. package/src/useApi/useApi.test.tsx +193 -0
  7. package/src/useApi/useApi.ts +58 -0
  8. package/src/useBodyScroll/useBodyScroll.test.ts +112 -0
  9. package/src/useBodyScroll/useBodyScroll.ts +89 -0
  10. package/src/useBreakpoint/useBreakpoint.test.tsx +45 -0
  11. package/src/useBreakpoint/useBreakpoint.ts +44 -0
  12. package/src/useCart/useCart.test.tsx +89 -0
  13. package/src/useCart/useCart.tsx +132 -0
  14. package/src/useCheckout/useCheckout.test.tsx +53 -0
  15. package/src/useCheckout/useCheckout.tsx +95 -0
  16. package/src/useClickAnyWhere/useClickAnyWhere.test.ts +15 -0
  17. package/src/useClickAnyWhere/useClickAnyWhere.ts +9 -0
  18. package/src/useClickOut/useClickOut.test.tsx +78 -0
  19. package/src/useClickOut/useClickOut.ts +25 -0
  20. package/src/useCurrency/useCurrency.test.tsx +33 -0
  21. package/src/useCurrency/useCurrency.ts +42 -0
  22. package/src/useCurrentState/useCurrentState.test.tsx +56 -0
  23. package/src/useCurrentState/useCurrentState.ts +22 -0
  24. package/src/useDateTimeFormat/useDateTimeFormat.test.tsx +47 -0
  25. package/src/useDateTimeFormat/useDateTimeFormat.ts +22 -0
  26. package/src/useDebounce/useDebounce.test.ts +49 -0
  27. package/src/useDebounce/useDebounce.ts +15 -0
  28. package/src/useDebounceCallback/useDebounceCallback.test.ts +34 -0
  29. package/src/useDebounceCallback/useDebounceCallback.ts +102 -0
  30. package/src/useDebugChangedProps/useDebugChangedProps.test.tsx +21 -0
  31. package/src/useDebugChangedProps/useDebugChangedProps.ts +17 -0
  32. package/src/useDomObserver/useDomObserver.test.ts +22 -0
  33. package/src/useDomObserver/useDomObserver.ts +26 -0
  34. package/src/useDrawer/useDrawer.test.ts +17 -0
  35. package/src/useDrawer/useDrawer.ts +16 -0
  36. package/src/useElementSize/useElementSize.test.ts +108 -0
  37. package/src/useElementSize/useElementSize.ts +34 -0
  38. package/src/useEventListener/useEventListener.test.ts +128 -0
  39. package/src/useEventListener/useEventListener.ts +43 -0
  40. package/src/useFilters/filter.service.ts +86 -0
  41. package/src/useFilters/filter.ts +152 -0
  42. package/src/useFilters/useFilters.test.ts +48 -0
  43. package/src/useFilters/useFilters.ts +44 -0
  44. package/src/useGenericContext/useGenericContext.test.tsx +19 -0
  45. package/src/useGenericContext/useGenericContext.ts +18 -0
  46. package/src/useGeolocation/geolocation.service.ts +66 -0
  47. package/src/useGeolocation/useGeolocation.test.ts +29 -0
  48. package/src/useGeolocation/useGeolocation.ts +56 -0
  49. package/src/useHttp/useHttp.test.tsx +192 -0
  50. package/src/useHttp/useHttp.ts +52 -0
  51. package/src/useIdle/useIdle.test.ts +31 -0
  52. package/src/useIdle/useIdle.ts +79 -0
  53. package/src/useInfiniteLoader/useInfiniteLoader.test.ts +41 -0
  54. package/src/useInfiniteLoader/useInfiniteLoader.ts +46 -0
  55. package/src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts +3 -0
  56. package/src/useKeyboard/codes.ts +94 -0
  57. package/src/useKeyboard/helper.ts +31 -0
  58. package/src/useKeyboard/useKeyboard.test.tsx +163 -0
  59. package/src/useKeyboard/useKeyboard.ts +91 -0
  60. package/src/useLabel/useLabel.test.tsx +68 -0
  61. package/src/useLabel/useLabel.tsx +43 -0
  62. package/src/useLayout/useLayout.test.tsx +32 -0
  63. package/src/useLayout/useLayout.tsx +19 -0
  64. package/src/useModal/useModal.test.ts +19 -0
  65. package/src/useModal/useModal.ts +16 -0
  66. package/src/useMounted/useMounted.test.tsx +34 -0
  67. package/src/useMounted/useMounted.ts +25 -0
  68. package/src/useNumber/useNumber.test.tsx +36 -0
  69. package/src/useNumber/useNumber.ts +15 -0
  70. package/src/usePage/usePage.test.tsx +31 -0
  71. package/src/usePage/usePage.tsx +19 -0
  72. package/src/usePagination/pagination.service.ts +23 -0
  73. package/src/usePagination/usePagination.test.ts +64 -0
  74. package/src/usePagination/usePagination.ts +47 -0
  75. package/src/usePortal/usePortal.test.ts +24 -0
  76. package/src/usePortal/usePortal.ts +34 -0
  77. package/src/usePrevious/usePrevious.test.ts +21 -0
  78. package/src/usePrevious/usePrevious.ts +9 -0
  79. package/src/useRealSize/useRealSize.ts +41 -0
  80. package/src/useResize/useResize.test.ts +31 -0
  81. package/src/useResize/useResize.ts +18 -0
  82. package/src/useSSR/useSSR.test.tsx +16 -0
  83. package/src/useSSR/useSSR.ts +23 -0
  84. package/src/useScroll/useScroll.test.tsx +25 -0
  85. package/src/useScroll/useScroll.ts +30 -0
  86. package/src/useScrollTo/useScrollTo.test.tsx +105 -0
  87. package/src/useScrollTo/useScrollTo.ts +41 -0
  88. package/src/useSearchParams/useSearchParams.test.ts +59 -0
  89. package/src/useSearchParams/useSearchParams.ts +123 -0
  90. package/src/useSearchParamsEncoded/useSearchParamsEncoded.test.ts +58 -0
  91. package/src/useSearchParamsEncoded/useSearchParamsEncoded.ts +202 -0
  92. package/src/useSorting/useSorting.test.ts +40 -0
  93. package/src/useSorting/useSorting.ts +99 -0
  94. package/src/useSwiperIndex/useSwiperIndex.ts +30 -0
  95. package/src/useSyncedRef/useSyncedRef.test.ts +16 -0
  96. package/src/useSyncedRef/useSyncedRef.ts +18 -0
  97. package/src/useUI/useUI.test.ts +16 -0
  98. package/src/useUI/useUI.ts +44 -0
  99. package/src/useUnit/useUnit.test.tsx +33 -0
  100. package/src/useUnit/useUnit.ts +5 -0
  101. package/src/useUnmountEffect/useUnmountEffect.test.ts +15 -0
  102. package/src/useUnmountEffect/useUnmountEffect.ts +16 -0
  103. package/src/useUser/useUser.test.ts +22 -0
  104. package/src/useUser/useUser.ts +20 -0
  105. package/src/useWindowSize/useWindowSize.test.ts +48 -0
  106. package/src/useWindowSize/useWindowSize.ts +31 -0
@@ -0,0 +1,64 @@
1
+ // readme https://testing-library.com/
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { usePagination } from './usePagination';
4
+
5
+ describe('usePagination', () => {
6
+
7
+ test('should paginate items', () => {
8
+ const items = new Array(100).fill(0).map((_, i) => i + 1);
9
+ const { result } = renderHook(() => usePagination(items));
10
+ expect(result.current.page).toEqual(1);
11
+ expect(result.current.pages).toEqual(7);
12
+ expect(result.current.total).toEqual(100);
13
+ expect(result.current.items.length).toEqual(15);
14
+ expect(result.current.items[0]).toEqual(1);
15
+ });
16
+
17
+ test('should paginate items with params', () => {
18
+ const items = new Array(100).fill(0).map((_, i) => i + 1);
19
+ const { result } = renderHook(() => usePagination(items, 2, 10));
20
+ expect(result.current.page).toEqual(2);
21
+ expect(result.current.pages).toEqual(10);
22
+ expect(result.current.total).toEqual(100);
23
+ expect(result.current.items.length).toEqual(10);
24
+ expect(result.current.items[0]).toEqual(11);
25
+ });
26
+
27
+ test('should call nextPage and paginate items', async () => {
28
+ const items = new Array(100).fill(0).map((_, i) => i + 1);
29
+ const { result } = renderHook(() => usePagination(items, 2, 10));
30
+ act(() => { result.current.nextPage(); });
31
+ expect(result.current.page).toEqual(3);
32
+ expect(result.current.items.length).toEqual(10);
33
+ expect(result.current.items[0]).toEqual(21);
34
+ });
35
+
36
+ test('should call previousPage and paginate items', async () => {
37
+ const items = new Array(100).fill(0).map((_, i) => i + 1);
38
+ const { result } = renderHook(() => usePagination(items, 2, 10));
39
+ act(() => { result.current.previousPage(); });
40
+ expect(result.current.page).toEqual(1);
41
+ expect(result.current.items.length).toEqual(10);
42
+ expect(result.current.items[0]).toEqual(1);
43
+ });
44
+
45
+ test('should call goToPage and paginate items', async () => {
46
+ const items = new Array(100).fill(0).map((_, i) => i + 1);
47
+ const { result } = renderHook(() => usePagination(items, 2, 10));
48
+ act(() => { result.current.goToPage(4); });
49
+ expect(result.current.page).toEqual(4);
50
+ expect(result.current.items.length).toEqual(10);
51
+ expect(result.current.items[0]).toEqual(31);
52
+ });
53
+
54
+ test('should call setPerPage and paginate items', async () => {
55
+ const items = new Array(100).fill(0).map((_, i) => i + 1);
56
+ const { result } = renderHook(() => usePagination(items, 2, 10));
57
+ act(() => { result.current.setPerPage(20); });
58
+ expect(result.current.pages).toEqual(5);
59
+ expect(result.current.page).toEqual(2);
60
+ expect(result.current.items.length).toEqual(20);
61
+ expect(result.current.items[0]).toEqual(21);
62
+ });
63
+
64
+ });
@@ -0,0 +1,47 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { getPagedItems } from './pagination.service';
3
+
4
+ export function usePagination<T>($items: T[], $page: number = 1, $perPage: number = 15) {
5
+
6
+ const total = $items.length;
7
+
8
+ const [page, setPage] = useState($page);
9
+ const [perPage, setPerPage_] = useState($perPage);
10
+ const [items, setItems] = useState(getPagedItems<T>($items, $page, $perPage));
11
+
12
+ const pages = Math.ceil(total / perPage);
13
+
14
+ useEffect(() => {
15
+ // console.log('setPagination');
16
+ setItems(getPagedItems<T>($items, page, perPage));
17
+ }, [$items]);
18
+
19
+ function goToPage(page: number) {
20
+ if (page > 0 && page <= pages) {
21
+ // console.log('goToPage', page);
22
+ setPage(page);
23
+ setItems(getPagedItems<T>($items, page, perPage));
24
+ }
25
+ }
26
+
27
+ function setPerPage(perPage: number) {
28
+ if (perPage > 0) {
29
+ // console.log('setPerPage', perPage);
30
+ const pages = Math.ceil(total / perPage);
31
+ const newPage = Math.max(1, Math.min(pages, page));
32
+ setPage(newPage);
33
+ setPerPage_(perPage);
34
+ setItems(getPagedItems<T>($items, newPage, perPage));
35
+ }
36
+ }
37
+
38
+ function nextPage() {
39
+ goToPage(page + 1);
40
+ }
41
+
42
+ function previousPage() {
43
+ goToPage(page - 1);
44
+ }
45
+
46
+ return { items, page, pages, total, nextPage, previousPage, goToPage, setPerPage };
47
+ }
@@ -0,0 +1,24 @@
1
+ // readme https://testing-library.com/
2
+ import { renderHook } from '@testing-library/react';
3
+ import { createElementWithId, getUID, usePortal } from './usePortal';
4
+
5
+ describe('usePortal', () => {
6
+
7
+ test('should return react portal', async () => {
8
+ const { result } = renderHook(() => usePortal());
9
+ expect(result.current).toBeInstanceOf(HTMLElement);
10
+ });
11
+
12
+ test('should create element with id', async () => {
13
+ const element = createElementWithId('test');
14
+ expect(element).toBeInstanceOf(HTMLElement);
15
+ expect(element).toHaveProperty('id', 'test');
16
+ });
17
+
18
+ test('should return UID', async () => {
19
+ const uid = getUID();
20
+ expect(uid).toEqual(expect.any(String));
21
+ expect(uid).toMatch(/^[\d|\w]{8}$/);
22
+ });
23
+
24
+ });
@@ -0,0 +1,34 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useSSR } from '../useSSR/useSSR';
3
+
4
+ const namespace = 'theme';
5
+
6
+ export function usePortal(name: string = getUID(), getContainer?: () => HTMLElement | null): HTMLElement | null {
7
+ const { isBrowser } = useSSR();
8
+
9
+ const id = `${namespace}-${name}`;
10
+
11
+ const [portal, setPortal] = useState<HTMLElement | null>(isBrowser ? createElementWithId(id) : null);
12
+
13
+ useEffect(() => {
14
+ const container = typeof getContainer === 'function' ? getContainer() : null;
15
+ const parentElement = container || document.body;
16
+ const element = parentElement.querySelector<HTMLElement>(`#${id}`) || createElementWithId(id);
17
+ if (!element.parentElement) {
18
+ parentElement.appendChild(element);
19
+ }
20
+ setPortal(element);
21
+ }, []);
22
+
23
+ return portal;
24
+ }
25
+
26
+ export function createElementWithId(id: string): HTMLElement {
27
+ const element = document.createElement('div');
28
+ element.setAttribute('id', id);
29
+ return element;
30
+ }
31
+
32
+ export function getUID() {
33
+ return Math.random().toString(32).slice(2, 10);
34
+ }
@@ -0,0 +1,21 @@
1
+ // readme https://testing-library.com/
2
+ import { renderHook } from '@testing-library/react';
3
+ import { usePrevious } from './usePrevious';
4
+
5
+ describe('usePrevious', () => {
6
+
7
+ test('should return previous value', () => {
8
+ const value = false;
9
+ const { result, unmount, rerender } = renderHook((props) => usePrevious<boolean>(props.value), { initialProps: { value } });
10
+ expect(result.current).toBe(null);
11
+ rerender({ value: true });
12
+ expect(result.current).toBe(false);
13
+ rerender({ value: false });
14
+ expect(result.current).toBe(true);
15
+ /*
16
+ unmount();
17
+ expect(result.current).toBe(null);
18
+ */
19
+ });
20
+
21
+ });
@@ -0,0 +1,9 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ export function usePrevious<T>(state: T): T | undefined | null {
4
+ const ref = useRef<T | null>(null);
5
+ useEffect(() => {
6
+ ref.current = state;
7
+ });
8
+ return ref.current; // ref ? ref.current : null;
9
+ }
@@ -0,0 +1,41 @@
1
+ import { MutableRefObject, useEffect, useState } from 'react';
2
+
3
+ export type SizeType = {
4
+ width: number;
5
+ height: number;
6
+ };
7
+
8
+ export type SizeResult = [SizeType, () => void];
9
+
10
+ export function useRealSize<T extends HTMLElement>(ref: MutableRefObject<T | null>): SizeResult {
11
+ const [state, setState] = useState<SizeType>({ width: 0, height: 0 });
12
+ const update = () => {
13
+ const { width, height } = getRealSize(ref.current);
14
+ setState({ width, height });
15
+ };
16
+ useEffect(() => update(), [ref.current]);
17
+ return [state, update];
18
+ }
19
+
20
+ export function getRealSize(element: HTMLElement | null): SizeType {
21
+ const defaultSize: SizeType = { width: 0, height: 0 };
22
+ if (!element || typeof window === 'undefined') {
23
+ return defaultSize;
24
+ }
25
+ const rect = element.getBoundingClientRect();
26
+ const { width, height } = window.getComputedStyle(element);
27
+ return {
28
+ width: getComputedValue(`${width}`, rect.width),
29
+ height: getComputedValue(`${height}`, rect.height),
30
+ };
31
+ }
32
+
33
+ function getComputedValue(style: string, parentNum: number): number {
34
+ if (!style) {
35
+ return 0;
36
+ }
37
+ const value = style.includes('px') ? +style.split('px')[0] : (
38
+ style.includes('%') ? +style.split('%')[0] * parentNum * 0.01 : style
39
+ );
40
+ return Number.isNaN(+value) ? 0 : +value;
41
+ }
@@ -0,0 +1,31 @@
1
+ // readme https://testing-library.com/
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { useResize } from './useResize';
4
+
5
+ const windowResize = (): void => {
6
+ window.dispatchEvent(new Event('resize'));
7
+ };
8
+
9
+ describe('useResize', () => {
10
+
11
+ it('should work correctly', () => {
12
+ const handler = jest.fn();
13
+ renderHook(() => useResize(handler));
14
+ expect(handler).toBeCalledTimes(1);
15
+ act(() => {
16
+ windowResize();
17
+ });
18
+ expect(handler).toBeCalledTimes(2);
19
+ });
20
+
21
+ it('should work correctly with immediate param set to false', () => {
22
+ const handler = jest.fn();
23
+ renderHook(() => useResize(handler, false));
24
+ expect(handler).toBeCalledTimes(0);
25
+ act(() => {
26
+ windowResize();
27
+ });
28
+ expect(handler).toBeCalledTimes(1);
29
+ });
30
+
31
+ });
@@ -0,0 +1,18 @@
1
+ import { useEventListener } from '../useEventListener/useEventListener';
2
+ import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
3
+
4
+ export function useResize(callback: () => unknown, immediate: boolean = true): void {
5
+
6
+ const handleResize = () => callback();
7
+
8
+ useEventListener('resize', handleResize);
9
+
10
+ // Set size at the first client-side load
11
+ useIsomorphicLayoutEffect(() => {
12
+ if (immediate) {
13
+ handleResize();
14
+ }
15
+ // eslint-disable-next-line react-hooks/exhaustive-deps
16
+ }, []);
17
+
18
+ }
@@ -0,0 +1,16 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { detectIsBrowser, useSSR } from './useSSR';
3
+
4
+ describe('useSSR', () => {
5
+ test('should return correctly', () => {
6
+ const { result } = renderHook(() => useSSR());
7
+ const { isBrowser, isServer } = result.current;
8
+ expect(isBrowser).toBe(true);
9
+ expect(isServer).toBe(false);
10
+ });
11
+
12
+ test('detectIsBrowser should return true', () => {
13
+ const isBrowser = detectIsBrowser();
14
+ expect(isBrowser).toBe(true);
15
+ });
16
+ });
@@ -0,0 +1,23 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export type SSRState = {
4
+ isBrowser: boolean;
5
+ isServer: boolean;
6
+ };
7
+
8
+ export const detectIsBrowser = (): boolean => {
9
+ return Boolean(
10
+ typeof window !== 'undefined' && window.document && window.document.createElement
11
+ );
12
+ };
13
+
14
+ export function useSSR(): SSRState {
15
+ const [browser, setBrowser] = useState<boolean>(false);
16
+ useEffect(() => {
17
+ setBrowser(detectIsBrowser());
18
+ }, []);
19
+ return {
20
+ isBrowser: browser,
21
+ isServer: !browser,
22
+ };
23
+ }
@@ -0,0 +1,25 @@
1
+ // readme https://testing-library.com/
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { useScroll } from './useScroll';
4
+
5
+ const windowScroll = (scrollTop: number): void => {
6
+ window.pageYOffset = scrollTop;
7
+ window.dispatchEvent(new Event('scroll'));
8
+ };
9
+
10
+ const wrapper = (props: { children: React.ReactNode }) => (
11
+ <div data-testid="test" style={{ height: '2000px' }}>{props.children}</div>
12
+ );
13
+
14
+ describe('useScroll', () => {
15
+
16
+ it('should work correctly', () => {
17
+ const { result } = renderHook(() => useScroll(), { wrapper });
18
+ expect(result.current).toStrictEqual({ left: 0, top: 0 });
19
+ act(() => {
20
+ windowScroll(500);
21
+ });
22
+ expect(result.current).toStrictEqual({ left: 0, top: 500 });
23
+ });
24
+
25
+ });
@@ -0,0 +1,30 @@
1
+ import { useState } from 'react';
2
+ import { useEventListener } from '../useEventListener/useEventListener';
3
+ import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
4
+
5
+ type Scroll = {
6
+ left: number;
7
+ top: number;
8
+ };
9
+
10
+ export function useScroll(): Scroll {
11
+ const [scroll, setScroll] = useState<Scroll>({ left: 0, top: 0 });
12
+
13
+ const onScroll = () => {
14
+ const left = window.pageXOffset;
15
+ const top = window.pageYOffset;
16
+ if (scroll.left !== left || scroll.top !== top) {
17
+ setScroll({ left, top });
18
+ }
19
+ };
20
+
21
+ useEventListener('scroll', onScroll);
22
+
23
+ // Set size at the first client-side load
24
+ useIsomorphicLayoutEffect(() => {
25
+ onScroll();
26
+ // eslint-disable-next-line react-hooks/exhaustive-deps
27
+ }, []);
28
+
29
+ return scroll;
30
+ }
@@ -0,0 +1,105 @@
1
+ // readme https://testing-library.com/
2
+ import { render, renderHook, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { forwardRef } from 'react';
5
+ import { useScrollTo } from './useScrollTo';
6
+
7
+ jest.mock('framer-motion', () => {
8
+ const FakeTransition = jest.fn().mockImplementation(({ children }) => children);
9
+ const FakeAnimate = jest.fn().mockImplementation((from, to, options) => {
10
+ if (options && typeof options.onUpdate === 'function') {
11
+ options.onUpdate(from);
12
+ options.onUpdate(to);
13
+ }
14
+ });
15
+ const FakeAnimatePresence = jest.fn().mockImplementation(({ children }) => (
16
+ <FakeTransition>{children}</FakeTransition>
17
+ ));
18
+ const motion = {
19
+ a: jest.fn().mockImplementation(({ children, ...rest }) => (
20
+ <a {...rest}>{children}</a>
21
+ )),
22
+ li: jest.fn().mockImplementation(({ children }) => children),
23
+ span: jest.fn().mockImplementation(({ children }) => children),
24
+ small: jest.fn().mockImplementation(({ children }) => <small>{children}</small>),
25
+ h4: jest.fn().mockImplementation(({ children }) => <h4>{children}</h4>),
26
+ h2: jest.fn().mockImplementation(({ children }) => <h2>{children}</h2>),
27
+ p: jest.fn().mockImplementation(({ children }) => <p>{children}</p>),
28
+ // eslint-disable-next-line react/display-name
29
+ div: forwardRef(({ children, ...rest }: { children: React.ReactNode }, ref) => {
30
+ const { whileTap, animate, initial, variants, ...divProps } = rest as any;
31
+ return (
32
+ <div {...divProps} ref={ref}>
33
+ {children}
34
+ </div>
35
+ );
36
+ }),
37
+ button: jest.fn().mockImplementation(({ children, ...rest }) => (
38
+ <button {...rest}>{children}</button>
39
+ )),
40
+ custom: jest.fn().mockImplementation((props) => props),
41
+ };
42
+ return {
43
+ __esModule: true,
44
+ motion: motion,
45
+ animate: FakeAnimate,
46
+ AnimatePresence: FakeAnimatePresence,
47
+ default: jest.fn(),
48
+ };
49
+ });
50
+
51
+ const Wrapper = () => {
52
+ const scrollTo = useScrollTo();
53
+ return (
54
+ <div>
55
+ <a data-testid="trigger" href="#target" onClick={scrollTo}>trigger</a>
56
+ <div style={{ height: '2000px' }}>spacer</div>
57
+ <div id="target" data-testid="target">target</div>
58
+ </div>
59
+ );
60
+ };
61
+
62
+ describe('useScrollTo', () => {
63
+
64
+ const SCROLL_TO = window.scrollTo;
65
+
66
+ let scrollTo: jest.SpyInstance<void, [x: number, y: number]> = jest.fn();
67
+
68
+ beforeEach(() => {
69
+ jest.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(() => ({
70
+ x: 0,
71
+ y: 0,
72
+ width: 0,
73
+ height: 100,
74
+ top: 1000,
75
+ right: 0,
76
+ bottom: 1100,
77
+ left: 0,
78
+ }) as any);
79
+ scrollTo = jest.spyOn(window, 'scrollTo').mockImplementation((left: number, top: number) => {
80
+ window.pageXOffset = left;
81
+ window.pageYOffset = top;
82
+ });
83
+ });
84
+
85
+ afterAll(() => {
86
+ window.scrollTo = SCROLL_TO;
87
+ });
88
+
89
+ it('should return a callback', () => {
90
+ const { result } = renderHook(() => useScrollTo());
91
+ expect(result.current).toEqual(expect.any(Function));
92
+ });
93
+
94
+ it('should work correctly', async () => {
95
+ render(<Wrapper />);
96
+ expect(window.pageYOffset).toBe(0);
97
+ const trigger = screen.getByTestId('trigger');
98
+ await userEvent.click(trigger);
99
+ expect(scrollTo).toBeCalled();
100
+ expect(scrollTo).toHaveBeenCalledTimes(2);
101
+ expect(scrollTo).toHaveBeenCalledWith(0, 940);
102
+ expect(window.pageYOffset).toBe(940);
103
+ });
104
+
105
+ });
@@ -0,0 +1,41 @@
1
+ import { animate } from 'framer-motion';
2
+
3
+ export function useScrollTo(): (event: React.MouseEvent<HTMLElement>) => void {
4
+ // The handler to smoothly scroll the element into view
5
+ const scrollToHash = (event: React.MouseEvent<HTMLElement>) => {
6
+ const href = event.currentTarget.getAttribute('href');
7
+ // console.log(event.currentTarget, href, href?.indexOf('#'));
8
+ if (href && href.indexOf('#') !== -1) {
9
+ const hashId = `#${href.split('#')[1]}`;
10
+ // console.log('hashId', hashId);
11
+ // console.log('NavLink.onClick', event.currentTarget.getAttribute('href'), hashId);
12
+ scrollToSelector(hashId);
13
+ }
14
+ };
15
+ return scrollToHash;
16
+ }
17
+
18
+ export function scrollToSelector(selector?: string, offset: number = 0) {
19
+ if (selector && typeof window !== 'undefined') {
20
+ const element = document.querySelector(selector);
21
+ if (element) {
22
+ const rect = element.getBoundingClientRect();
23
+ scrollToRelativeY(rect.top, offset);
24
+ }
25
+ }
26
+ }
27
+
28
+ export function scrollToRelativeY(y: number = 0, offset: number = 0) {
29
+ const to = window.pageYOffset + y - 60 + offset;
30
+ return scrollToY(to);
31
+ }
32
+
33
+ export function scrollToY(y: number = 0) {
34
+ const from = window.pageYOffset;
35
+ const to = y;
36
+ const duration = Math.sqrt(Math.abs(to - from) / 4000);
37
+ animate(from, to, {
38
+ duration,
39
+ onUpdate: v => window.scrollTo(0, v)
40
+ });
41
+ }
@@ -0,0 +1,59 @@
1
+
2
+ // readme https://testing-library.com/
3
+ import { act, renderHook } from '@testing-library/react';
4
+ import * as nextRouter from 'next/router';
5
+ import { useSearchParams } from './useSearchParams';
6
+
7
+ const useRouter = (nextRouter as any).useRouter = jest.fn();
8
+
9
+ describe('useSearchParams', () => {
10
+
11
+ it('should work correctly', async () => {
12
+ useRouter.mockReturnValue({
13
+ asPath: '',
14
+ });
15
+ const params = {};
16
+ const { result } = renderHook(() => useSearchParams());
17
+ expect(result.current.params).toEqual(params);
18
+ });
19
+
20
+ it('should work correctly with params', async () => {
21
+ useRouter.mockReturnValue({
22
+ asPath: '?flag=1',
23
+ });
24
+ const params = { flag: '1' };
25
+ const { result } = renderHook(() => useSearchParams());
26
+ expect(result.current.params).toEqual(params);
27
+ });
28
+
29
+ it('should replace search params silently', async () => {
30
+ useRouter.mockReturnValue({
31
+ asPath: '',
32
+ });
33
+ const params = { flag: '1' };
34
+ const { result } = renderHook(() => useSearchParams());
35
+ window.history.replaceState = jest.fn();
36
+ expect(result.current.replaceParamsSilently).toEqual(expect.any(Function));
37
+ act(() => result.current.replaceParamsSilently(params));
38
+ expect(window.history.replaceState).toBeCalledWith('flag=1', '', 'http://localhost/?flag=1');
39
+ });
40
+
41
+ it('should replace search params', async () => {
42
+ const handler = jest.fn();
43
+ useRouter.mockReturnValue({
44
+ asPath: '',
45
+ replace: handler,
46
+ });
47
+ const params = { flag: '1' };
48
+ const { result } = renderHook(() => useSearchParams());
49
+ window.history.replaceState = jest.fn();
50
+ expect(result.current.replaceParams).toEqual(expect.any(Function));
51
+ act(() => result.current.replaceParams(params));
52
+ expect(handler).toBeCalledTimes(1);
53
+ expect(handler).toBeCalledWith({
54
+ pathname: '',
55
+ query: { flag: '1' }
56
+ });
57
+ });
58
+
59
+ });