@musecat/functionkit 1.0.0 → 1.0.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/.agents/references/components/ScrolltoTop.md +36 -0
- package/.agents/references/components/SwitchCase.md +47 -0
- package/.agents/references/components/ViewportPortal.md +34 -0
- package/.agents/references/cookie/cookie.md +47 -0
- package/.agents/references/datetime/dateTime.client.md +42 -0
- package/.agents/references/datetime/dateTime.server.md +42 -0
- package/.agents/references/datetime/dateTime.shared.md +86 -0
- package/.agents/references/hooks/useAvoidKeyboard.md +29 -0
- package/.agents/references/hooks/useCheckInvisible.md +36 -0
- package/.agents/references/hooks/useCheckScroll.md +36 -0
- package/.agents/references/hooks/useClientDateTime.md +39 -0
- package/.agents/references/hooks/useDebounce.md +46 -0
- package/.agents/references/hooks/useDebouncedCallback.md +34 -0
- package/.agents/references/hooks/useDoubleClick.md +40 -0
- package/.agents/references/hooks/useGeolocation.md +37 -0
- package/.agents/references/hooks/useHasMounted.md +27 -0
- package/.agents/references/hooks/useIntersectionObserver.md +37 -0
- package/.agents/references/hooks/useInterval.md +40 -0
- package/.agents/references/hooks/useKeyboardHeight.md +33 -0
- package/.agents/references/hooks/useKeyboardListNavigation.md +58 -0
- package/.agents/references/hooks/useLongPress.md +48 -0
- package/.agents/references/hooks/usePreservedCallback.md +33 -0
- package/.agents/references/hooks/usePreservedReference.md +34 -0
- package/.agents/references/hooks/useRefEffect.md +35 -0
- package/.agents/references/hooks/useRelativeDateTime.md +35 -0
- package/.agents/references/hooks/useTimeout.md +42 -0
- package/.agents/references/hooks/useToggleState.md +39 -0
- package/.agents/references/hooks/useViewportHeight.md +26 -0
- package/.agents/references/hooks/useViewportMatch.md +32 -0
- package/.agents/references/utils/browserStorage.md +41 -0
- package/.agents/references/utils/buildContext.md +47 -0
- package/.agents/references/utils/clipboardShare.md +59 -0
- package/.agents/references/utils/floatingMotion.md +67 -0
- package/.agents/references/utils/getDeviceInfo.md +43 -0
- package/.agents/references/utils/isEditableKeyboardTarget.md +35 -0
- package/.agents/references/utils/mergeRefs.md +29 -0
- package/.agents/references/utils/seen.md +37 -0
- package/.agents/references/utils/subscribeKeyboardHeight.md +35 -0
- package/AGENTS.md +103 -396
- package/LICENSE +9 -0
- package/README.md +38 -0
- package/index.ts +97 -102
- package/package.json +27 -13
- package/{components → packages/components}/ScrolltoTop.tsx +3 -3
- package/{components → packages/components}/SwitchCase.tsx +7 -7
- package/packages/components/ViewportPortal.tsx +40 -0
- package/packages/cookie/cookie.shared.ts +138 -0
- package/packages/datetime/dateTime.client.ts +108 -0
- package/packages/datetime/dateTime.server.ts +108 -0
- package/packages/datetime/dateTime.shared.ts +358 -0
- package/packages/hooks/useAvoidKeyboard.ts +28 -0
- package/packages/hooks/useCheckInvisible.ts +25 -0
- package/packages/hooks/useCheckScroll.ts +19 -0
- package/packages/hooks/useClientDateTime.ts +49 -0
- package/packages/hooks/useDebounce.ts +118 -0
- package/packages/hooks/useDebouncedCallback.ts +69 -0
- package/packages/hooks/useDoubleClick.ts +53 -0
- package/packages/hooks/useGeolocation.ts +146 -0
- package/packages/hooks/useHasMounted.ts +13 -0
- package/packages/hooks/useIntersectionObserver.ts +34 -0
- package/packages/hooks/useInterval.ts +56 -0
- package/packages/hooks/useKeyboardHeight.ts +18 -0
- package/packages/hooks/useKeyboardListNavigation.ts +170 -0
- package/packages/hooks/useLongPress.ts +120 -0
- package/packages/hooks/usePreservedCallback.ts +21 -0
- package/packages/hooks/usePreservedReference.ts +21 -0
- package/packages/hooks/useRefEffect.ts +35 -0
- package/packages/hooks/useRelativeDateTime.ts +54 -0
- package/packages/hooks/useTimeout.ts +56 -0
- package/packages/hooks/useToggleState.ts +13 -0
- package/packages/hooks/useViewportHeight.ts +23 -0
- package/packages/hooks/useViewportMatch.ts +27 -0
- package/packages/utils/browserStorage.ts +59 -0
- package/packages/utils/buildContext.tsx +19 -0
- package/packages/utils/checkDevice.ts +74 -0
- package/packages/utils/clipboardShare.tsx +47 -0
- package/{utils → packages/utils}/clipboardShare.types.ts +7 -7
- package/packages/utils/floatingMotion.ts +99 -0
- package/packages/utils/keyboardTarget.ts +13 -0
- package/packages/utils/mergeRefs.ts +15 -0
- package/packages/utils/seen.ts +29 -0
- package/packages/utils/subscribeKeyboardHeight.ts +54 -0
- package/tests/components/ScrolltoTop.test.tsx +15 -0
- package/tests/components/SwitchCase.test.tsx +34 -0
- package/tests/components/ViewportPortal.test.tsx +42 -0
- package/tests/cookie/cookie.test.ts +156 -0
- package/tests/datetime/datetime.test.ts +250 -0
- package/tests/hooks/useAvoidKeyboard.test.ts +12 -0
- package/tests/hooks/useCheckInvisible.test.ts +28 -0
- package/tests/hooks/useCheckScroll.test.ts +23 -0
- package/tests/hooks/useClientDateTime.test.ts +14 -0
- package/tests/hooks/useDebounce.test.ts +71 -0
- package/tests/hooks/useDebouncedCallback.test.ts +34 -0
- package/tests/hooks/useDoubleClick.test.ts +31 -0
- package/tests/hooks/useGeolocation.test.ts +50 -0
- package/tests/hooks/useHasMounted.test.ts +10 -0
- package/tests/hooks/useIntersectionObserver.test.ts +13 -0
- package/tests/hooks/useInterval.test.ts +31 -0
- package/tests/hooks/useKeyboardHeight.test.ts +10 -0
- package/tests/hooks/useKeyboardListNavigation.test.ts +82 -0
- package/tests/hooks/useLongPress.test.ts +52 -0
- package/tests/hooks/usePreservedCallback.test.ts +35 -0
- package/tests/hooks/usePreservedReference.test.ts +35 -0
- package/tests/hooks/useRefEffect.test.ts +30 -0
- package/tests/hooks/useRelativeDateTime.test.ts +19 -0
- package/tests/hooks/useTimeout.test.ts +34 -0
- package/tests/hooks/useToggleState.test.ts +35 -0
- package/tests/hooks/useViewportHeight.test.ts +10 -0
- package/tests/hooks/useViewportMatch.test.ts +35 -0
- package/tests/setup.ts +9 -0
- package/tests/utils/browserStorage.test.ts +54 -0
- package/tests/utils/buildContext.test.tsx +27 -0
- package/tests/utils/checkDevice.test.ts +84 -0
- package/tests/utils/clipboardShare.test.ts +78 -0
- package/tests/utils/floatingMotion.test.ts +84 -0
- package/tests/utils/keyboardTarget.test.ts +44 -0
- package/tests/utils/mergeRefs.test.ts +18 -0
- package/tests/utils/seen.test.ts +31 -0
- package/tests/utils/subscribeKeyboardHeight.test.ts +50 -0
- package/tsconfig.json +16 -16
- package/vitest.config.ts +16 -0
- package/components/ViewportPortal.tsx +0 -40
- package/cookie/cookie.shared.ts +0 -142
- package/datetime/dateTime.client.ts +0 -108
- package/datetime/dateTime.server.ts +0 -108
- package/datetime/dateTime.shared.ts +0 -358
- package/hooks/useAvoidKeyboard.ts +0 -28
- package/hooks/useCheckInvisible.ts +0 -25
- package/hooks/useCheckScroll.ts +0 -19
- package/hooks/useClientDateTime.ts +0 -49
- package/hooks/useDebounce.ts +0 -118
- package/hooks/useDebouncedCallback.ts +0 -69
- package/hooks/useDoubleClick.ts +0 -53
- package/hooks/useGeolocation.ts +0 -146
- package/hooks/useHasMounted.ts +0 -13
- package/hooks/useIntersectionObserver.ts +0 -34
- package/hooks/useInterval.ts +0 -55
- package/hooks/useKeyboardHeight.ts +0 -18
- package/hooks/useKeyboardListNavigation.ts +0 -170
- package/hooks/useLongPress.ts +0 -120
- package/hooks/usePreservedCallback.ts +0 -21
- package/hooks/usePreservedReference.ts +0 -21
- package/hooks/useRefEffect.ts +0 -35
- package/hooks/useRelativeDateTime.ts +0 -54
- package/hooks/useTimeout.ts +0 -56
- package/hooks/useToggleState.ts +0 -13
- package/hooks/useViewportHeight.ts +0 -23
- package/hooks/useViewportMatch.ts +0 -33
- package/utils/browserStorage.ts +0 -59
- package/utils/buildContext.tsx +0 -19
- package/utils/checkDevice.ts +0 -74
- package/utils/clipboardShare.tsx +0 -47
- package/utils/floatingMotion.ts +0 -99
- package/utils/keyboardTarget.ts +0 -13
- package/utils/mergeRefs.ts +0 -15
- package/utils/seen.ts +0 -29
- package/utils/subscribeKeyboardHeight.ts +0 -54
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
4
|
-
|
|
5
|
-
import { debounce } from "./useDebounce";
|
|
6
|
-
import { usePreservedCallback } from "./usePreservedCallback";
|
|
7
|
-
|
|
8
|
-
type DebounceOptions = {
|
|
9
|
-
leading?: boolean;
|
|
10
|
-
trailing?: boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function useDebouncedCallback({
|
|
14
|
-
onChange,
|
|
15
|
-
timeThreshold,
|
|
16
|
-
leading = false,
|
|
17
|
-
trailing = true,
|
|
18
|
-
}: DebounceOptions & {
|
|
19
|
-
onChange: (newValue: boolean) => void;
|
|
20
|
-
timeThreshold: number;
|
|
21
|
-
}) {
|
|
22
|
-
const handleChange = usePreservedCallback(onChange);
|
|
23
|
-
const ref = useRef({ value: false, clearPreviousDebounce: () => {} });
|
|
24
|
-
|
|
25
|
-
useEffect(function clearDebouncedOnUnmount() {
|
|
26
|
-
const current = ref.current;
|
|
27
|
-
return () => {
|
|
28
|
-
current.clearPreviousDebounce();
|
|
29
|
-
};
|
|
30
|
-
}, []);
|
|
31
|
-
|
|
32
|
-
const edges = useMemo(() => {
|
|
33
|
-
const _edges: Array<"leading" | "trailing"> = [];
|
|
34
|
-
if (leading) {
|
|
35
|
-
_edges.push("leading");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (trailing) {
|
|
39
|
-
_edges.push("trailing");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return _edges;
|
|
43
|
-
}, [leading, trailing]);
|
|
44
|
-
|
|
45
|
-
return useCallback(
|
|
46
|
-
(nextValue: boolean) => {
|
|
47
|
-
if (nextValue === ref.current.value) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const debounced = debounce(
|
|
52
|
-
() => {
|
|
53
|
-
handleChange(nextValue);
|
|
54
|
-
|
|
55
|
-
ref.current.value = nextValue;
|
|
56
|
-
},
|
|
57
|
-
timeThreshold,
|
|
58
|
-
{ edges },
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
ref.current.clearPreviousDebounce();
|
|
62
|
-
|
|
63
|
-
debounced();
|
|
64
|
-
|
|
65
|
-
ref.current.clearPreviousDebounce = debounced.cancel;
|
|
66
|
-
},
|
|
67
|
-
[handleChange, timeThreshold, edges],
|
|
68
|
-
);
|
|
69
|
-
}
|
package/hooks/useDoubleClick.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { type MouseEvent, useCallback, useEffect, useRef } from "react";
|
|
4
|
-
|
|
5
|
-
type UseDoubleClickProps<E extends HTMLElement> = {
|
|
6
|
-
delay?: number;
|
|
7
|
-
click?: (event: MouseEvent<E>) => void;
|
|
8
|
-
doubleClick: (event: MouseEvent<E>) => void;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function useDoubleClick<E extends HTMLElement = HTMLElement>({
|
|
12
|
-
delay = 250,
|
|
13
|
-
click,
|
|
14
|
-
doubleClick,
|
|
15
|
-
}: UseDoubleClickProps<E>) {
|
|
16
|
-
const clickTimeout = useRef<number | null>(null);
|
|
17
|
-
|
|
18
|
-
const savedClick = useRef(click);
|
|
19
|
-
const savedDoubleClick = useRef(doubleClick);
|
|
20
|
-
savedClick.current = click;
|
|
21
|
-
savedDoubleClick.current = doubleClick;
|
|
22
|
-
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
return () => {
|
|
25
|
-
if (clickTimeout.current != null) {
|
|
26
|
-
window.clearTimeout(clickTimeout.current);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
}, []);
|
|
30
|
-
|
|
31
|
-
const handleEvent = useCallback(
|
|
32
|
-
(event: MouseEvent<E>) => {
|
|
33
|
-
if (clickTimeout.current != null) {
|
|
34
|
-
window.clearTimeout(clickTimeout.current);
|
|
35
|
-
clickTimeout.current = null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (event.detail === 1 && savedClick.current) {
|
|
39
|
-
clickTimeout.current = window.setTimeout(() => {
|
|
40
|
-
clickTimeout.current = null;
|
|
41
|
-
savedClick.current?.(event);
|
|
42
|
-
}, delay);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (event.detail === 2) {
|
|
46
|
-
savedDoubleClick.current(event);
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
[delay],
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
return handleEvent;
|
|
53
|
-
}
|
package/hooks/useGeolocation.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
-
|
|
5
|
-
type GeolocationData = {
|
|
6
|
-
latitude: number;
|
|
7
|
-
longitude: number;
|
|
8
|
-
accuracy: number;
|
|
9
|
-
altitude: number | null;
|
|
10
|
-
altitudeAccuracy: number | null;
|
|
11
|
-
heading: number | null;
|
|
12
|
-
speed: number | null;
|
|
13
|
-
timestamp: number;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const MOUNT_BEHAVIOR = { GET: "get", WATCH: "watch" } as const;
|
|
17
|
-
|
|
18
|
-
type MountBehavior = (typeof MOUNT_BEHAVIOR)[keyof typeof MOUNT_BEHAVIOR];
|
|
19
|
-
|
|
20
|
-
type UseGeolocationOptions = {
|
|
21
|
-
mountBehavior?: MountBehavior;
|
|
22
|
-
enableHighAccuracy?: boolean;
|
|
23
|
-
timeout?: number;
|
|
24
|
-
maximumAge?: number;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export function useGeolocation(options?: UseGeolocationOptions) {
|
|
28
|
-
const [state, setState] = useState<{
|
|
29
|
-
loading: boolean;
|
|
30
|
-
error: string | null;
|
|
31
|
-
data: GeolocationData | null;
|
|
32
|
-
}>({
|
|
33
|
-
loading: !!options?.mountBehavior,
|
|
34
|
-
error: null,
|
|
35
|
-
data: null,
|
|
36
|
-
});
|
|
37
|
-
const [isTracking, setIsTracking] = useState(false);
|
|
38
|
-
const watchIdRef = useRef<number | null>(null);
|
|
39
|
-
|
|
40
|
-
const isSupported = useCallback(() => {
|
|
41
|
-
if (typeof window === "undefined" || !navigator.geolocation) {
|
|
42
|
-
setState((prev) => ({
|
|
43
|
-
...prev,
|
|
44
|
-
loading: false,
|
|
45
|
-
error: "Geolocation is not supported by this environment.",
|
|
46
|
-
}));
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
return true;
|
|
50
|
-
}, []);
|
|
51
|
-
|
|
52
|
-
const handleSuccess = useCallback((position: GeolocationPosition) => {
|
|
53
|
-
const { coords } = position;
|
|
54
|
-
setState((prev) => ({
|
|
55
|
-
...prev,
|
|
56
|
-
loading: false,
|
|
57
|
-
error: null,
|
|
58
|
-
data: {
|
|
59
|
-
latitude: coords.latitude,
|
|
60
|
-
longitude: coords.longitude,
|
|
61
|
-
accuracy: coords.accuracy,
|
|
62
|
-
altitude: coords.altitude,
|
|
63
|
-
altitudeAccuracy: coords.altitudeAccuracy,
|
|
64
|
-
heading: coords.heading,
|
|
65
|
-
speed: coords.speed,
|
|
66
|
-
timestamp: position.timestamp,
|
|
67
|
-
},
|
|
68
|
-
}));
|
|
69
|
-
}, []);
|
|
70
|
-
|
|
71
|
-
const handleError = useCallback((error: GeolocationPositionError) => {
|
|
72
|
-
setState((prev) => ({
|
|
73
|
-
...prev,
|
|
74
|
-
loading: false,
|
|
75
|
-
error: error.message,
|
|
76
|
-
}));
|
|
77
|
-
}, []);
|
|
78
|
-
|
|
79
|
-
const geoOptions = useCallback(
|
|
80
|
-
() => ({
|
|
81
|
-
enableHighAccuracy: options?.enableHighAccuracy ?? false,
|
|
82
|
-
maximumAge: options?.maximumAge ?? 0,
|
|
83
|
-
timeout: options?.timeout ?? Infinity,
|
|
84
|
-
}),
|
|
85
|
-
[options?.enableHighAccuracy, options?.maximumAge, options?.timeout],
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const getCurrentPosition = useCallback(() => {
|
|
89
|
-
if (!isSupported()) return;
|
|
90
|
-
setState((prev) => ({ ...prev, loading: true }));
|
|
91
|
-
navigator.geolocation.getCurrentPosition(
|
|
92
|
-
handleSuccess,
|
|
93
|
-
handleError,
|
|
94
|
-
geoOptions(),
|
|
95
|
-
);
|
|
96
|
-
}, [handleSuccess, handleError, geoOptions, isSupported]);
|
|
97
|
-
|
|
98
|
-
const startTracking = useCallback(() => {
|
|
99
|
-
if (!isSupported()) return;
|
|
100
|
-
|
|
101
|
-
if (watchIdRef.current !== null) {
|
|
102
|
-
navigator.geolocation.clearWatch(watchIdRef.current);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
setState((prev) => ({ ...prev, loading: true }));
|
|
106
|
-
|
|
107
|
-
watchIdRef.current = navigator.geolocation.watchPosition(
|
|
108
|
-
(position) => {
|
|
109
|
-
setIsTracking(true);
|
|
110
|
-
handleSuccess(position);
|
|
111
|
-
},
|
|
112
|
-
handleError,
|
|
113
|
-
geoOptions(),
|
|
114
|
-
);
|
|
115
|
-
}, [handleSuccess, handleError, geoOptions, isSupported]);
|
|
116
|
-
|
|
117
|
-
const stopTracking = useCallback(() => {
|
|
118
|
-
if (watchIdRef.current === null) return;
|
|
119
|
-
navigator.geolocation.clearWatch(watchIdRef.current);
|
|
120
|
-
watchIdRef.current = null;
|
|
121
|
-
setIsTracking(false);
|
|
122
|
-
}, []);
|
|
123
|
-
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
if (options?.mountBehavior === MOUNT_BEHAVIOR.WATCH) {
|
|
126
|
-
startTracking();
|
|
127
|
-
} else if (options?.mountBehavior === MOUNT_BEHAVIOR.GET) {
|
|
128
|
-
getCurrentPosition();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return () => {
|
|
132
|
-
if (watchIdRef.current !== null) {
|
|
133
|
-
navigator.geolocation.clearWatch(watchIdRef.current);
|
|
134
|
-
watchIdRef.current = null;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
}, [options?.mountBehavior, getCurrentPosition, startTracking]);
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
...state,
|
|
141
|
-
getCurrentPosition,
|
|
142
|
-
startTracking,
|
|
143
|
-
stopTracking,
|
|
144
|
-
isTracking,
|
|
145
|
-
};
|
|
146
|
-
}
|
package/hooks/useHasMounted.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useMemo } from "react";
|
|
4
|
-
|
|
5
|
-
import { usePreservedCallback } from "./usePreservedCallback";
|
|
6
|
-
import { useRefEffect } from "./useRefEffect";
|
|
7
|
-
|
|
8
|
-
export function useIntersectionObserver<Element extends HTMLElement>(
|
|
9
|
-
callback: (entry: IntersectionObserverEntry) => void,
|
|
10
|
-
options: IntersectionObserverInit,
|
|
11
|
-
): (element: Element | null) => void {
|
|
12
|
-
const preservedCallback = usePreservedCallback(callback);
|
|
13
|
-
|
|
14
|
-
const observer = useMemo(() => {
|
|
15
|
-
if (typeof IntersectionObserver === "undefined") {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return new IntersectionObserver(([entry]) => {
|
|
20
|
-
preservedCallback(entry);
|
|
21
|
-
}, options);
|
|
22
|
-
}, [preservedCallback, options]);
|
|
23
|
-
|
|
24
|
-
return useRefEffect<Element>(
|
|
25
|
-
(element) => {
|
|
26
|
-
observer?.observe(element);
|
|
27
|
-
|
|
28
|
-
return () => {
|
|
29
|
-
observer?.unobserve(element);
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
[preservedCallback, options],
|
|
33
|
-
);
|
|
34
|
-
}
|
package/hooks/useInterval.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef } from "react";
|
|
4
|
-
|
|
5
|
-
import { usePreservedCallback } from "./usePreservedCallback";
|
|
6
|
-
|
|
7
|
-
type IntervalOptions =
|
|
8
|
-
| number
|
|
9
|
-
| {
|
|
10
|
-
delay: number;
|
|
11
|
-
immediate?: boolean;
|
|
12
|
-
enabled?: boolean;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export function useInterval(callback: () => void, options: IntervalOptions) {
|
|
16
|
-
const delay = typeof options === "number" ? options : options.delay;
|
|
17
|
-
const immediate = typeof options === "number" ? false : options.immediate;
|
|
18
|
-
const enabled = typeof options === "number" ? true : (options.enabled ?? true);
|
|
19
|
-
|
|
20
|
-
const preservedCallback = usePreservedCallback(callback);
|
|
21
|
-
const immediateCalledRef = useRef(false);
|
|
22
|
-
|
|
23
|
-
useEffect(
|
|
24
|
-
function runImmediateCallback() {
|
|
25
|
-
if (immediate !== true) {
|
|
26
|
-
immediateCalledRef.current = false;
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!enabled) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (immediateCalledRef.current) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
immediateCalledRef.current = true;
|
|
39
|
-
preservedCallback();
|
|
40
|
-
},
|
|
41
|
-
[immediate, preservedCallback, enabled],
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
useEffect(
|
|
45
|
-
function startInterval() {
|
|
46
|
-
if (!enabled) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const id = setInterval(preservedCallback, delay);
|
|
51
|
-
return () => clearInterval(id);
|
|
52
|
-
},
|
|
53
|
-
[delay, preservedCallback, enabled],
|
|
54
|
-
);
|
|
55
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { subscribeKeyboardHeight } from "../utils/subscribeKeyboardHeight";
|
|
5
|
-
|
|
6
|
-
export function useKeyboardHeight() {
|
|
7
|
-
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
8
|
-
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
const { unsubscribe } = subscribeKeyboardHeight({
|
|
11
|
-
callback: setKeyboardHeight,
|
|
12
|
-
immediate: true,
|
|
13
|
-
});
|
|
14
|
-
return unsubscribe;
|
|
15
|
-
}, []);
|
|
16
|
-
|
|
17
|
-
return { keyboardHeight };
|
|
18
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
-
import { isEditableKeyboardTarget } from "../utils/keyboardTarget";
|
|
5
|
-
|
|
6
|
-
const FIRST_ITEM_KEYS = new Set(["ArrowDown", "ArrowRight", "Home"]);
|
|
7
|
-
const LAST_ITEM_KEYS = new Set(["ArrowUp", "ArrowLeft", "End"]);
|
|
8
|
-
|
|
9
|
-
type UseKeyboardListNavigationOptions = {
|
|
10
|
-
itemCount: number;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function useKeyboardListNavigation({
|
|
14
|
-
itemCount,
|
|
15
|
-
}: UseKeyboardListNavigationOptions) {
|
|
16
|
-
const [activeIndex, setActiveIndex] = useState(-1);
|
|
17
|
-
const itemRefs = useRef<(HTMLElement | null)[]>([]);
|
|
18
|
-
|
|
19
|
-
const setItemRef = useCallback(
|
|
20
|
-
(index: number, element: HTMLElement | null) => {
|
|
21
|
-
itemRefs.current[index] = element;
|
|
22
|
-
},
|
|
23
|
-
[],
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
const focusItem = useCallback((index: number) => {
|
|
27
|
-
const target = itemRefs.current[index];
|
|
28
|
-
if (!target) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
setActiveIndex(index);
|
|
33
|
-
target.focus();
|
|
34
|
-
target.scrollIntoView({
|
|
35
|
-
block: "nearest",
|
|
36
|
-
inline: "nearest",
|
|
37
|
-
});
|
|
38
|
-
return true;
|
|
39
|
-
}, []);
|
|
40
|
-
|
|
41
|
-
const focusBoundaryItem = useCallback(
|
|
42
|
-
(key: string) => {
|
|
43
|
-
if (itemCount === 0) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (FIRST_ITEM_KEYS.has(key)) {
|
|
48
|
-
return focusItem(0);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (LAST_ITEM_KEYS.has(key)) {
|
|
52
|
-
return focusItem(itemCount - 1);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return false;
|
|
56
|
-
},
|
|
57
|
-
[focusItem, itemCount],
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const moveActiveItem = useCallback(
|
|
61
|
-
(key: string) => {
|
|
62
|
-
if (itemCount === 0) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (key === "Home") {
|
|
67
|
-
return focusItem(0);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (key === "End") {
|
|
71
|
-
return focusItem(itemCount - 1);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (key === "ArrowDown" || key === "ArrowRight") {
|
|
75
|
-
const nextIndex = activeIndex < 0 ? 0 : (activeIndex + 1) % itemCount;
|
|
76
|
-
return focusItem(nextIndex);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (key === "ArrowUp" || key === "ArrowLeft") {
|
|
80
|
-
const nextIndex =
|
|
81
|
-
activeIndex < 0
|
|
82
|
-
? itemCount - 1
|
|
83
|
-
: (activeIndex - 1 + itemCount) % itemCount;
|
|
84
|
-
return focusItem(nextIndex);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return false;
|
|
88
|
-
},
|
|
89
|
-
[activeIndex, focusItem, itemCount],
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const clickActiveItem = useCallback(() => {
|
|
93
|
-
if (activeIndex < 0) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
itemRefs.current[activeIndex]?.click();
|
|
98
|
-
return true;
|
|
99
|
-
}, [activeIndex]);
|
|
100
|
-
|
|
101
|
-
const handleListKeyDown = useCallback(
|
|
102
|
-
(
|
|
103
|
-
event: React.KeyboardEvent<HTMLElement>,
|
|
104
|
-
options?: { container?: HTMLElement | null },
|
|
105
|
-
) => {
|
|
106
|
-
if (itemCount === 0 || event.defaultPrevented) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const target = event.target as HTMLElement | null;
|
|
111
|
-
|
|
112
|
-
if (!target) {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (isEditableKeyboardTarget(target)) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (options?.container && !options.container.contains(target)) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (event.key === "Escape") {
|
|
125
|
-
setActiveIndex(-1);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
event.key === "Enter" ||
|
|
131
|
-
event.key === " " ||
|
|
132
|
-
event.key === "ArrowDown" ||
|
|
133
|
-
event.key === "ArrowUp" ||
|
|
134
|
-
event.key === "ArrowLeft" ||
|
|
135
|
-
event.key === "ArrowRight" ||
|
|
136
|
-
event.key === "Home" ||
|
|
137
|
-
event.key === "End"
|
|
138
|
-
) {
|
|
139
|
-
event.preventDefault();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
143
|
-
clickActiveItem();
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (moveActiveItem(event.key)) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (focusBoundaryItem(event.key)) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
[clickActiveItem, moveActiveItem, focusBoundaryItem, itemCount],
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
useEffect(() => {
|
|
159
|
-
itemRefs.current = itemRefs.current.slice(0, itemCount);
|
|
160
|
-
}, [itemCount]);
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
activeIndex,
|
|
164
|
-
setActiveIndex,
|
|
165
|
-
setItemRef,
|
|
166
|
-
clickActiveItem,
|
|
167
|
-
focusBoundaryItem,
|
|
168
|
-
handleListKeyDown,
|
|
169
|
-
};
|
|
170
|
-
}
|
package/hooks/useLongPress.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { type MouseEvent, type TouchEvent, useCallback, useRef } from "react";
|
|
4
|
-
|
|
5
|
-
export type UseLongPressOptions<E extends HTMLElement> = {
|
|
6
|
-
delay?: number;
|
|
7
|
-
moveThreshold?: {
|
|
8
|
-
x?: number;
|
|
9
|
-
y?: number;
|
|
10
|
-
};
|
|
11
|
-
onClick?: (event: MouseEvent<E> | TouchEvent<E>) => void;
|
|
12
|
-
onLongPressEnd?: (event: MouseEvent<E> | TouchEvent<E>) => void;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export function useLongPress<E extends HTMLElement = HTMLElement>(
|
|
16
|
-
onLongPress: (event: MouseEvent<E> | TouchEvent<E>) => void,
|
|
17
|
-
{
|
|
18
|
-
delay = 500,
|
|
19
|
-
moveThreshold,
|
|
20
|
-
onClick,
|
|
21
|
-
onLongPressEnd,
|
|
22
|
-
}: UseLongPressOptions<E> = {},
|
|
23
|
-
) {
|
|
24
|
-
const timeoutRef = useRef<number | null>(null);
|
|
25
|
-
const isLongPressActiveRef = useRef(false);
|
|
26
|
-
const initialPositionRef = useRef({ x: 0, y: 0 });
|
|
27
|
-
|
|
28
|
-
const savedOnLongPress = useRef(onLongPress);
|
|
29
|
-
const savedOnClick = useRef(onClick);
|
|
30
|
-
const savedOnLongPressEnd = useRef(onLongPressEnd);
|
|
31
|
-
savedOnLongPress.current = onLongPress;
|
|
32
|
-
savedOnClick.current = onClick;
|
|
33
|
-
savedOnLongPressEnd.current = onLongPressEnd;
|
|
34
|
-
|
|
35
|
-
const hasThreshold =
|
|
36
|
-
moveThreshold?.x !== undefined || moveThreshold?.y !== undefined;
|
|
37
|
-
|
|
38
|
-
const getClientPosition = useCallback(
|
|
39
|
-
(event: MouseEvent<E> | TouchEvent<E>) => {
|
|
40
|
-
if ("touches" in event.nativeEvent) {
|
|
41
|
-
const touch = event.nativeEvent.touches[0];
|
|
42
|
-
return { x: touch.clientX, y: touch.clientY };
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
x: event.nativeEvent.clientX,
|
|
46
|
-
y: event.nativeEvent.clientY,
|
|
47
|
-
};
|
|
48
|
-
},
|
|
49
|
-
[],
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const isMovedBeyondThreshold = useCallback(
|
|
53
|
-
(event: MouseEvent<E> | TouchEvent<E>) => {
|
|
54
|
-
const { x, y } = getClientPosition(event);
|
|
55
|
-
const deltaX = Math.abs(x - initialPositionRef.current.x);
|
|
56
|
-
const deltaY = Math.abs(y - initialPositionRef.current.y);
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
(moveThreshold?.x !== undefined && deltaX > moveThreshold.x) ||
|
|
60
|
-
(moveThreshold?.y !== undefined && deltaY > moveThreshold.y)
|
|
61
|
-
);
|
|
62
|
-
},
|
|
63
|
-
[getClientPosition, moveThreshold],
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
const cancelLongPress = useCallback(() => {
|
|
67
|
-
if (timeoutRef.current !== null) {
|
|
68
|
-
window.clearTimeout(timeoutRef.current);
|
|
69
|
-
timeoutRef.current = null;
|
|
70
|
-
}
|
|
71
|
-
}, []);
|
|
72
|
-
|
|
73
|
-
const handlePressStart = useCallback(
|
|
74
|
-
(event: MouseEvent<E> | TouchEvent<E>) => {
|
|
75
|
-
cancelLongPress();
|
|
76
|
-
initialPositionRef.current = getClientPosition(event);
|
|
77
|
-
isLongPressActiveRef.current = false;
|
|
78
|
-
|
|
79
|
-
timeoutRef.current = window.setTimeout(() => {
|
|
80
|
-
isLongPressActiveRef.current = true;
|
|
81
|
-
savedOnLongPress.current(event);
|
|
82
|
-
}, delay);
|
|
83
|
-
},
|
|
84
|
-
[cancelLongPress, delay, getClientPosition],
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const handlePressEnd = useCallback(
|
|
88
|
-
(event: MouseEvent<E> | TouchEvent<E>) => {
|
|
89
|
-
if (isLongPressActiveRef.current) {
|
|
90
|
-
savedOnLongPressEnd.current?.(event);
|
|
91
|
-
} else if (timeoutRef.current !== null) {
|
|
92
|
-
savedOnClick.current?.(event);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
cancelLongPress();
|
|
96
|
-
isLongPressActiveRef.current = false;
|
|
97
|
-
},
|
|
98
|
-
[cancelLongPress],
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const handlePressMove = useCallback(
|
|
102
|
-
(event: MouseEvent<E> | TouchEvent<E>) => {
|
|
103
|
-
if (timeoutRef.current !== null && isMovedBeyondThreshold(event)) {
|
|
104
|
-
cancelLongPress();
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
[cancelLongPress, isMovedBeyondThreshold],
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
onMouseDown: handlePressStart,
|
|
112
|
-
onMouseUp: handlePressEnd,
|
|
113
|
-
onMouseLeave: cancelLongPress,
|
|
114
|
-
onTouchStart: handlePressStart,
|
|
115
|
-
onTouchEnd: handlePressEnd,
|
|
116
|
-
...(hasThreshold
|
|
117
|
-
? { onTouchMove: handlePressMove, onMouseMove: handlePressMove }
|
|
118
|
-
: {}),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
4
|
-
|
|
5
|
-
export function usePreservedCallback<
|
|
6
|
-
Arguments extends unknown[] = unknown[],
|
|
7
|
-
ReturnValue = unknown,
|
|
8
|
-
>(callback: (...args: Arguments) => ReturnValue) {
|
|
9
|
-
const callbackRef = useRef(callback);
|
|
10
|
-
|
|
11
|
-
useEffect(
|
|
12
|
-
function syncCallbackRef() {
|
|
13
|
-
callbackRef.current = callback;
|
|
14
|
-
},
|
|
15
|
-
[callback],
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
return useCallback((...args: Arguments) => {
|
|
19
|
-
return callbackRef.current(...args);
|
|
20
|
-
}, []);
|
|
21
|
-
}
|