@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,43 @@
1
+ import { RefObject, useEffect, useRef } from 'react';
2
+ import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
3
+
4
+ // Window Event based useEventListener interface
5
+ function useEventListener_<K extends keyof WindowEventMap>(eventName: K, handler: (event: WindowEventMap[K]) => void, element?: undefined, options?: boolean | AddEventListenerOptions): void;
6
+ // Element Event based useEventListener interface
7
+ function useEventListener_<K extends keyof HTMLElementEventMap, T extends HTMLElement = HTMLDivElement>(eventName: K, handler: (event: HTMLElementEventMap[K]) => void, element: RefObject<T>, options?: boolean | AddEventListenerOptions): void;
8
+ // Document Event based useEventListener interface
9
+ function useEventListener_<K extends keyof DocumentEventMap>(eventName: K, handler: (event: DocumentEventMap[K]) => void, element: RefObject<Document>, options?: boolean | AddEventListenerOptions): void;
10
+ function useEventListener_<KW extends keyof WindowEventMap, KH extends keyof HTMLElementEventMap, T extends HTMLElement | void = void>(
11
+ eventName: KW | KH,
12
+ handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void,
13
+ element?: RefObject<T>,
14
+ options?: boolean | AddEventListenerOptions
15
+ ) {
16
+ // Create a ref that stores handler
17
+ const savedHandler = useRef(handler);
18
+
19
+ useIsomorphicLayoutEffect(() => {
20
+ savedHandler.current = handler;
21
+ }, [handler]);
22
+
23
+ useEffect(() => {
24
+ // Define the listening target
25
+ const targetElement: T | Window = element?.current || window;
26
+
27
+ if (!(targetElement && targetElement.addEventListener)) {
28
+ return;
29
+ }
30
+
31
+ // Create event listener that calls handler function stored in ref
32
+ const eventListener: typeof handler = event => savedHandler.current(event);
33
+
34
+ targetElement.addEventListener(eventName, eventListener, options);
35
+
36
+ // Remove event listener on cleanup
37
+ return () => {
38
+ targetElement.removeEventListener(eventName, eventListener);
39
+ };
40
+ }, [eventName, element, options]);
41
+ }
42
+
43
+ export const useEventListener = useEventListener_;
@@ -0,0 +1,86 @@
1
+ import { IEquatable } from '@websolutespa/bom-core';
2
+ import { IFeatureType } from '@websolutespa/bom-mixer-models';
3
+ import { Filter, FilterMode, FilterParams } from './filter';
4
+
5
+ export function getFilters<T>(items: T[], featureTypes: IFeatureType[], filterMap: (key: string, item: T, value: IEquatable) => boolean, params?: FilterParams | null): Filter<T>[] {
6
+ return featureTypes.map(featureType => {
7
+ const filter = Filter.fromFeatureType<T>(featureType, (featureType.mode as FilterMode) || FilterMode.OR);
8
+ filter.filter = (item, value) => {
9
+ if (typeof filterMap === 'function') {
10
+ return filterMap(featureType.id, item, value);
11
+ }
12
+ return false;
13
+ };
14
+ filter.removeInvalidOptions(items);
15
+ if (params && params[filter.id] != null) {
16
+ filter.values = params[filter.id] as IEquatable[];
17
+ }
18
+ return filter;
19
+ });
20
+ }
21
+
22
+ export function getFilteredItems<T>(items: T[], selectedFilters: Filter<T>[]): T[] {
23
+ const filteredItems = items.filter(item => {
24
+ let has = true;
25
+ selectedFilters.forEach(filter => {
26
+ has = has && filter.match(item);
27
+ });
28
+ return has;
29
+ });
30
+ return filteredItems;
31
+ }
32
+
33
+ export function setFilters<T>(items: T[], filters: Filter<T>[], filter?: Filter<T>, values?: IEquatable[]): T[] {
34
+
35
+ // if passed featureType and values with set filter values
36
+ if (filter) {
37
+ // console.log('setFilter', filter.id, values);
38
+ filter.values = values || [];
39
+ }
40
+
41
+ // selecting all filters with values
42
+ const selectedFilters = filters.filter(x => x.values.length > 0);
43
+
44
+ // filtering items
45
+ const filteredItems = getFilteredItems<T>(items, selectedFilters);
46
+
47
+ // updating filter options
48
+ filters.forEach(filter => {
49
+ const otherFilters = selectedFilters.filter(x => x !== filter);
50
+ const filteredItems = getFilteredItems<T>(items, otherFilters);
51
+ if (filter.options) {
52
+ filter.options.forEach(option => {
53
+ let count = 0;
54
+ if (option.id) {
55
+ let i = 0;
56
+ while (i < filteredItems.length) {
57
+ const item = filteredItems[i];
58
+ if (filter.filter(item, option.id)) {
59
+ count++;
60
+ }
61
+ i++;
62
+ }
63
+ } else {
64
+ count = filteredItems.length;
65
+ }
66
+ option.count = count;
67
+ option.disabled = count === 0;
68
+ });
69
+ }
70
+ });
71
+
72
+ return filteredItems;
73
+ }
74
+
75
+ export function filtersToParams<T>(filters: Filter<T>[]): FilterParams | null {
76
+ const params: FilterParams = {};
77
+ let any = false;
78
+ filters.filter(x => x.hasAny()).forEach(filter => {
79
+ params[filter.id] = filter.values;
80
+ any = true;
81
+ });
82
+ if (!any) {
83
+ return null;
84
+ }
85
+ return params;
86
+ }
@@ -0,0 +1,152 @@
1
+ import { IEquatable } from '@websolutespa/bom-core';
2
+ import { IFeatureType } from '@websolutespa/bom-mixer-models';
3
+
4
+ export type FilterParams = { [key: string]: IEquatable[] | null };
5
+ export type FilterValues = IEquatable[][];
6
+
7
+ export enum FilterMode {
8
+ SELECT = 'select',
9
+ AND = 'and',
10
+ OR = 'or',
11
+ QUERY = 'query',
12
+ }
13
+
14
+ export type IFilterOption = {
15
+ id: IEquatable;
16
+ title: string;
17
+ count?: number;
18
+ disabled?: boolean;
19
+ };
20
+
21
+ export type IFilter = {
22
+ id?: string;
23
+ title?: string;
24
+ mode?: FilterMode;
25
+ options?: IFilterOption[];
26
+ values?: IEquatable[];
27
+ };
28
+
29
+ export class Filter<T> implements IFilter {
30
+ id: string = 'filter';
31
+ title: string = 'Filter';
32
+ mode: FilterMode = FilterMode.OR;
33
+ options: IFilterOption[] = [];
34
+ values: IEquatable[] = [];
35
+
36
+ constructor(options?: IFilter) {
37
+ if (options) {
38
+ // console.log(options.values);
39
+ Object.assign(this, options);
40
+ }
41
+ if (this.mode === FilterMode.SELECT) {
42
+ this.options.unshift({
43
+ id: -1, // !!! should be undefined
44
+ title: 'select',
45
+ });
46
+ }
47
+ }
48
+
49
+ static fromFeatureType<T>(featureType: IFeatureType, mode: FilterMode = FilterMode.AND): Filter<T> {
50
+ return new Filter<T>({
51
+ id: featureType.id,
52
+ title: featureType.title,
53
+ mode,
54
+ options: featureType.features,
55
+ });
56
+ }
57
+
58
+ filter(item: T, value: IEquatable): boolean {
59
+ console.warn('filter not defined for', item, value);
60
+ return true;
61
+ }
62
+
63
+ match(item: T): boolean {
64
+ let match: boolean;
65
+ if (this.mode === FilterMode.OR) {
66
+ // OR: if item has any of the selected values
67
+ match = this.values.length ? false : true;
68
+ this.values.forEach(value => {
69
+ match = match || this.filter(item, value);
70
+ });
71
+ } else {
72
+ // AND: item must have all selected values
73
+ match = true;
74
+ this.values.forEach(value => {
75
+ match = match && this.filter(item, value);
76
+ });
77
+ }
78
+ return match;
79
+ }
80
+
81
+ removeInvalidOptions(items: T[]): void {
82
+ if (this.options) {
83
+ this.options = this.options.filter(option => {
84
+ const hasItemWithOption = items.reduce((p, item) => {
85
+ return p || this.filter(item, option.id);
86
+ }, false);
87
+ return hasItemWithOption;
88
+ });
89
+ }
90
+ }
91
+
92
+ getLabel(): string | null {
93
+ if (this.hasAny()) {
94
+ return this.options.filter(x => x.id && this.values.indexOf(x.id) !== -1).map(x => x.title).join(', ');
95
+ } else {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ hasAny(): boolean {
101
+ return this.values.length > 0;
102
+ }
103
+
104
+ getCount(): number {
105
+ return this.options.reduce((p, option) => {
106
+ return p + (option.count || 0);
107
+ }, 0);
108
+ }
109
+
110
+ hasResults(): boolean {
111
+ return this.getCount() > 0;
112
+ }
113
+
114
+ has(option: IFilterOption): boolean {
115
+ return this.values.indexOf(option.id) !== -1;
116
+ }
117
+
118
+ set(option: IFilterOption): void {
119
+ // console.log(this.mode, FilterMode.SELECT);
120
+ if (this.mode === FilterMode.QUERY) {
121
+ this.values = option ? [option.toString()] : [];
122
+ } else if (this.mode === FilterMode.SELECT) {
123
+ this.values = option.id != null ? [option.id] : [];
124
+ } else {
125
+ const index = this.values.indexOf(option.id);
126
+ if (index === -1) {
127
+ if (option.id != null) {
128
+ this.values.push(option.id);
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ remove(option: IFilterOption): void {
135
+ const index = this.values.indexOf(option.id);
136
+ if (index !== -1) {
137
+ this.values.splice(index, 1);
138
+ }
139
+ }
140
+
141
+ toggle(option: IFilterOption): void {
142
+ if (this.has(option)) {
143
+ this.remove(option);
144
+ } else {
145
+ this.set(option);
146
+ }
147
+ }
148
+
149
+ clear(): void {
150
+ this.values = [];
151
+ }
152
+ }
@@ -0,0 +1,48 @@
1
+ // readme https://testing-library.com/
2
+ import { renderHook } from '@testing-library/react';
3
+ import { IEquatable } from '@websolutespa/bom-core';
4
+ import { act } from 'react-dom/test-utils';
5
+ import { useFilters } from './useFilters';
6
+
7
+ type Item = {
8
+ category: number;
9
+ };
10
+
11
+ describe('useFilters', () => {
12
+
13
+ const items = [{ category: 1 }, { category: 2 }];
14
+
15
+ const featureTypes = [{
16
+ id: 'category',
17
+ title: 'category',
18
+ mode: 'or',
19
+ features: [{
20
+ id: 1,
21
+ title: 'feature',
22
+ }]
23
+ }];
24
+
25
+ const filterItem = jest.fn((key: string, item: Item, value: IEquatable) => {
26
+ switch (key) {
27
+ case 'category':
28
+ return item.category === value;
29
+ }
30
+ return true;
31
+ });
32
+
33
+ it('should work correctly', async () => {
34
+ const params = {};
35
+ const { result } = renderHook(() => useFilters<Item>(items, featureTypes, filterItem, params));
36
+ expect(result.current.filteredItems).toEqual(items);
37
+ act(() => result.current.setFilter(result.current.filters[0], [1]));
38
+ expect(result.current.filteredItems).toEqual(items.filter(x => filterItem('category', x, 1)));
39
+ const omittedItems = result.current.itemsWithOmittedKeys('category');
40
+ expect(omittedItems).toEqual(items);
41
+ });
42
+
43
+ it('should work with params', async () => {
44
+ const params = { category: [1] };
45
+ const { result } = renderHook(() => useFilters<Item>(items, featureTypes, filterItem, params));
46
+ expect(result.current.filteredItems).toEqual(items.filter(x => filterItem('category', x, 1)));
47
+ });
48
+ });
@@ -0,0 +1,44 @@
1
+ import { IEquatable } from '@websolutespa/bom-core';
2
+ import { IFeatureType } from '@websolutespa/bom-mixer-models';
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { Filter, FilterParams } from './filter';
5
+ import { getFilters, setFilters } from './filter.service';
6
+
7
+ export function useFilters<T>(items: T[], featureTypes: IFeatureType[], filterItem: (key: string, item: T, value: IEquatable) => boolean, initialValues?: FilterParams | null): UseFiltersResultType<T> {
8
+ // console.log('useFilters', params);
9
+
10
+ // creating filters with useMemo because is an heavy operation
11
+ const filters = useMemo(() => {
12
+ return getFilters<T>(items, featureTypes, filterItem, initialValues);
13
+ // eslint-disable-next-line react-hooks/exhaustive-deps
14
+ }, [items, featureTypes, filterItem]);
15
+
16
+ const [filteredItems, setFilteredItems] = useState<T[]>(() => setFilters(items, filters));
17
+
18
+ const itemsWithOmittedKeys = useCallback((...keys: string[]) => {
19
+ const filtersWithOmittedKeys = filters.filter(x => !keys.includes(x.id));
20
+ const filteredItems = setFilters(items, filtersWithOmittedKeys);
21
+ return filteredItems;
22
+ }, [items, filters]);
23
+
24
+ // setFilter is called when user select a filter value
25
+ const setFilter = useCallback((filter?: Filter<T>, values?: IEquatable[]) => {
26
+ const filteredItems = setFilters(items, filters, filter, values);
27
+ setFilteredItems(filteredItems);
28
+ }, [items, filters]);
29
+
30
+ // initial call to setFilter
31
+ useEffect(() => {
32
+ setFilter();
33
+ return () => { };
34
+ }, [setFilter]);
35
+
36
+ return { filteredItems, filters, setFilter, itemsWithOmittedKeys };
37
+ }
38
+
39
+ export type UseFiltersResultType<T> = {
40
+ filteredItems: T[];
41
+ filters: Filter<T>[];
42
+ setFilter: (filter?: Filter<T>, values?: IEquatable[]) => void;
43
+ itemsWithOmittedKeys: (...keys: string[]) => T[];
44
+ };
@@ -0,0 +1,19 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { createGenericContext } from './useGenericContext';
3
+
4
+ describe('useGenericContext', () => {
5
+ beforeEach(() => {
6
+ jest.spyOn(console, 'error').mockImplementation(() => { });
7
+ });
8
+
9
+ test('should create context', async () => {
10
+ const value = { message: 'test' };
11
+ const [useContext, ContextProvider] = createGenericContext<typeof value>();
12
+ const wrapper = (props: { children: React.ReactNode }) => (
13
+ <ContextProvider value={value}>{props.children}</ContextProvider>
14
+ );
15
+ const { result } = renderHook(() => useContext(), { wrapper });
16
+ expect(result.current).toEqual(value);
17
+ });
18
+
19
+ });
@@ -0,0 +1,18 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export const createGenericContext = <T>(defaultContext: T | null = null) => {
4
+ // Create a context with a generic parameter or undefined
5
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6
+ const genericContext = createContext<T | undefined>(defaultContext!);
7
+
8
+ // Check if the value provided to the context is defined or throw an error
9
+ const useGenericContext = () => {
10
+ const context = useContext(genericContext);
11
+ if (!context) {
12
+ throw new Error('useGenericContext must be used within a Provider');
13
+ }
14
+ return context;
15
+ };
16
+
17
+ return [useGenericContext, genericContext.Provider] as const;
18
+ };
@@ -0,0 +1,66 @@
1
+
2
+ /**
3
+ * @desc Made compatible with {GeolocationPositionError} and {PositionError} cause
4
+ * PositionError been renamed to GeolocationPositionError in typescript 4.1.x and making
5
+ * own compatible interface is most easiest way to avoid errors.
6
+ */
7
+ export type IGeolocationPositionError = {
8
+ readonly code: number;
9
+ readonly message: string;
10
+ readonly PERMISSION_DENIED: number;
11
+ readonly POSITION_UNAVAILABLE: number;
12
+ readonly TIMEOUT: number;
13
+ };
14
+
15
+ export type GeoLocationSensorState = {
16
+ loading: boolean;
17
+ accuracy: number | null;
18
+ altitude: number | null;
19
+ altitudeAccuracy: number | null;
20
+ heading: number | null;
21
+ latitude: number | null;
22
+ longitude: number | null;
23
+ speed: number | null;
24
+ timestamp: number | null;
25
+ error?: Error | IGeolocationPositionError;
26
+ };
27
+
28
+ export function getCurrentPosition(options?: PositionOptions): Promise<GeoLocationSensorState> {
29
+ return new Promise<GeoLocationSensorState>((resolve, reject) => {
30
+ if (!hasGeolocation()) {
31
+ reject('geolocation not available');
32
+ }
33
+ navigator.geolocation.getCurrentPosition((event: GeolocationPosition) => {
34
+ const result: GeoLocationSensorState = {
35
+ loading: false,
36
+ accuracy: event.coords.accuracy,
37
+ altitude: event.coords.altitude,
38
+ altitudeAccuracy: event.coords.altitudeAccuracy,
39
+ heading: event.coords.heading,
40
+ latitude: event.coords.latitude,
41
+ longitude: event.coords.longitude,
42
+ speed: event.coords.speed,
43
+ timestamp: event.timestamp,
44
+ };
45
+ resolve(result);
46
+ }, (error: IGeolocationPositionError) => {
47
+ const sensorError: GeoLocationSensorState = {
48
+ loading: false,
49
+ error,
50
+ accuracy: null,
51
+ altitude: null,
52
+ altitudeAccuracy: null,
53
+ heading: null,
54
+ latitude: null,
55
+ longitude: null,
56
+ speed: null,
57
+ timestamp: Date.now(),
58
+ };
59
+ reject(sensorError);
60
+ }, options);
61
+ });
62
+ }
63
+
64
+ export function hasGeolocation(): boolean {
65
+ return Boolean(navigator && navigator.geolocation);
66
+ }
@@ -0,0 +1,29 @@
1
+ // readme https://testing-library.com/
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useGeolocation } from './useGeolocation';
4
+
5
+ const getCurrentPosition = jest.fn();
6
+ const watchPosition = jest.fn();
7
+ const clearWatch = jest.fn();
8
+
9
+ (global.navigator as any).geolocation = {
10
+ getCurrentPosition,
11
+ watchPosition,
12
+ clearWatch,
13
+ };
14
+
15
+ describe('useGeolocation', () => {
16
+
17
+ it('should work correctly', () => {
18
+ getCurrentPosition.mockImplementationOnce((success: (result: any) => Promise<any>) => Promise.resolve(success({
19
+ coords: {
20
+ latitude: 50.0,
21
+ longitude: 40.0
22
+ }
23
+ })));
24
+ const { result } = renderHook(() => useGeolocation());
25
+ expect(result.current.latitude).toBe(50.0);
26
+ expect(result.current.longitude).toBe(40.0);
27
+ });
28
+
29
+ });
@@ -0,0 +1,56 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { GeoLocationSensorState, hasGeolocation, IGeolocationPositionError } from './geolocation.service';
3
+
4
+ export function useGeolocation(options?: PositionOptions): GeoLocationSensorState {
5
+
6
+ const [state, setState] = useState<GeoLocationSensorState>({
7
+ loading: true,
8
+ accuracy: null,
9
+ altitude: null,
10
+ altitudeAccuracy: null,
11
+ heading: null,
12
+ latitude: null,
13
+ longitude: null,
14
+ speed: null,
15
+ timestamp: Date.now(),
16
+ });
17
+
18
+ let mounted = true;
19
+ let watchId: number;
20
+
21
+ const onEvent: PositionCallback = (event: GeolocationPosition) => {
22
+ if (mounted) {
23
+ setState({
24
+ loading: false,
25
+ accuracy: event.coords.accuracy,
26
+ altitude: event.coords.altitude,
27
+ altitudeAccuracy: event.coords.altitudeAccuracy,
28
+ heading: event.coords.heading,
29
+ latitude: event.coords.latitude,
30
+ longitude: event.coords.longitude,
31
+ speed: event.coords.speed,
32
+ timestamp: event.timestamp,
33
+ });
34
+ }
35
+ };
36
+
37
+ const onEventError = (error: IGeolocationPositionError) =>
38
+ mounted && setState((oldState) => ({ ...oldState, loading: false, error }));
39
+
40
+ useEffect(() => {
41
+ if (!hasGeolocation()) {
42
+ return;
43
+ }
44
+ navigator.geolocation.getCurrentPosition(onEvent, onEventError, options);
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ watchId = navigator.geolocation.watchPosition(onEvent, onEventError, options);
47
+
48
+ return () => {
49
+ // eslint-disable-next-line react-hooks/exhaustive-deps
50
+ mounted = false;
51
+ navigator.geolocation.clearWatch(watchId);
52
+ };
53
+ }, []);
54
+
55
+ return state;
56
+ }