@journ/lift 0.1.1 → 0.2.0
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/README.md +39 -9
- package/dist/env-C0PNnbwt.js +4 -0
- package/dist/logger-Dse1660l.js +33 -0
- package/dist/prefers-reduced-motion.d.ts +3 -2
- package/dist/prefers-reduced-motion.js +1 -3
- package/dist/required-context.d.ts +3 -2
- package/dist/required-context.js +2 -2
- package/dist/try-catch-D4d3N9_W.js +29 -0
- package/dist/types-p0KNRcj3.d.ts +4 -0
- package/dist/use-battery.d.ts +12 -0
- package/dist/use-battery.js +64 -0
- package/dist/use-browser.d.ts +6 -0
- package/dist/use-browser.js +51 -0
- package/dist/use-callback-ref.d.ts +5 -0
- package/dist/use-callback-ref.js +11 -0
- package/dist/use-controllable-state.d.ts +3 -2
- package/dist/use-controllable-state.js +6 -4
- package/dist/use-debounce-state.d.ts +9 -0
- package/dist/use-debounce-state.js +28 -0
- package/dist/use-debounced-callback.d.ts +16 -0
- package/dist/use-debounced-callback.js +68 -0
- package/dist/use-debounced-value.d.ts +3 -2
- package/dist/use-debounced-value.js +3 -3
- package/dist/use-did-update.d.ts +5 -0
- package/dist/use-did-update.js +24 -0
- package/dist/use-disclosure.d.ts +15 -0
- package/dist/use-disclosure.js +16 -0
- package/dist/use-draft.d.ts +7 -6
- package/dist/use-draft.js +2 -2
- package/dist/use-haptic.d.ts +8 -0
- package/dist/use-haptic.js +84 -0
- package/dist/use-headroom.d.ts +18 -0
- package/dist/use-headroom.js +65 -0
- package/dist/use-idle-queue.d.ts +28 -0
- package/dist/use-idle-queue.js +221 -0
- package/dist/use-intersection-observer.d.ts +17 -0
- package/dist/use-intersection-observer.js +50 -0
- package/dist/use-isomorphic-effect.d.ts +5 -0
- package/dist/use-isomorphic-effect.js +5 -0
- package/dist/use-keybindings.d.ts +14 -0
- package/dist/use-keybindings.js +76 -0
- package/dist/use-local-storage.d.ts +13 -0
- package/dist/use-local-storage.js +76 -0
- package/dist/use-logger.d.ts +4 -0
- package/dist/use-logger.js +17 -0
- package/dist/use-long-press.d.ts +12 -0
- package/dist/use-long-press.js +43 -0
- package/dist/use-map.d.ts +4 -0
- package/dist/use-map.js +30 -0
- package/dist/use-measure.d.ts +8 -0
- package/dist/use-measure.js +27 -0
- package/dist/use-media-query.d.ts +14 -0
- package/dist/use-media-query.js +24 -0
- package/dist/use-mouse.d.ts +18 -0
- package/dist/use-mouse.js +40 -0
- package/dist/use-network.d.ts +17 -0
- package/dist/use-network.js +38 -0
- package/dist/use-page-leave.d.ts +4 -0
- package/dist/use-page-leave.js +11 -0
- package/dist/use-pagination.d.ts +19 -0
- package/dist/use-pagination.js +55 -0
- package/dist/use-reduced-motion.d.ts +4 -0
- package/dist/use-reduced-motion.js +5 -0
- package/dist/use-scroll-direction.d.ts +5 -0
- package/dist/use-scroll-direction.js +33 -0
- package/dist/use-uncontrolled.d.ts +11 -0
- package/dist/use-uncontrolled.js +21 -0
- package/dist/use-validated-state.d.ts +10 -0
- package/dist/use-validated-state.js +22 -0
- package/dist/use-window-event.d.ts +5 -0
- package/dist/use-window-event.js +11 -0
- package/dist/use-window-scroll.d.ts +10 -0
- package/dist/use-window-scroll.js +33 -0
- package/package.json +149 -9
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @journ/lift
|
|
2
2
|
|
|
3
|
-
React hooks for state and
|
|
3
|
+
React hooks for state, timing and browser APIs, built on fitter-happier. Hooks marked * read `window`, `document` or `navigator` and need a browser; the others run anywhere React does.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -11,7 +11,8 @@ pnpm add @journ/lift react
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
|
-
import { useControllableState
|
|
14
|
+
import { useControllableState } from '@journ/lift/use-controllable-state'
|
|
15
|
+
import { useDebouncedValue } from '@journ/lift/use-debounced-value'
|
|
15
16
|
|
|
16
17
|
function Toggle(props: { pressed?: boolean; defaultPressed?: boolean; onPressedChange?: (pressed: boolean) => void }) {
|
|
17
18
|
const [pressed, setPressed] = useControllableState(
|
|
@@ -32,10 +33,39 @@ function Search({ query }: { query: string }) {
|
|
|
32
33
|
}
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
| Entry
|
|
36
|
-
|
|
|
37
|
-
| `@journ/lift/use-
|
|
38
|
-
| `@journ/lift/use-
|
|
39
|
-
| `@journ/lift/use-
|
|
40
|
-
| `@journ/lift/
|
|
41
|
-
| `@journ/lift/
|
|
36
|
+
| Entry | Exports | Notes |
|
|
37
|
+
| --------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
38
|
+
| `@journ/lift/use-battery` | `useBattery()` | Battery level and charging state * |
|
|
39
|
+
| `@journ/lift/use-browser` | `useBrowser()`, `getBrowserVersion()` | The detected browser and its major version * |
|
|
40
|
+
| `@journ/lift/use-callback-ref` | `useCallbackRef(fn)` | A stable callback that always calls the latest `fn` |
|
|
41
|
+
| `@journ/lift/use-controllable-state` | `useControllableState(value, defaultValue, onChange)` | |
|
|
42
|
+
| `@journ/lift/use-debounce-state` | `useDebouncedState(initial, wait, options)` | State whose setter debounces updates * |
|
|
43
|
+
| `@journ/lift/use-debounced-callback` | `useDebouncedCallback(fn, delayOrOptions)` | A debounced `fn` with `flush`, `cancel` and `isPending` * |
|
|
44
|
+
| `@journ/lift/use-debounced-value` | `useDebouncedValue(value, delay)` | |
|
|
45
|
+
| `@journ/lift/use-did-update` | `useDidUpdate(effect, deps)` | An effect that skips the first run |
|
|
46
|
+
| `@journ/lift/use-disclosure` | `useDisclosure(initial, options)` | Open, close and toggle state with `onOpen` and `onClose` |
|
|
47
|
+
| `@journ/lift/use-draft` | `useDraft` | Text typed freely that becomes a value on commit |
|
|
48
|
+
| `@journ/lift/use-haptic` | `useHaptic`, `useHapticsConfirm`, `useHapticsError`, `supportsHaptics` | Vibration and iOS haptic feedback * |
|
|
49
|
+
| `@journ/lift/use-headroom` | `useHeadroom(options)` | Pinned state and scroll progress for a header that hides on scroll down * |
|
|
50
|
+
| `@journ/lift/use-idle-queue` | `useIdleQueue(options)` | A queue that runs tasks while the browser is idle * |
|
|
51
|
+
| `@journ/lift/use-intersection-observer` | `useIntersectionObserver(options)` | Whether an element intersects the viewport * |
|
|
52
|
+
| `@journ/lift/use-isomorphic-effect` | `useIsomorphicEffect` | `useLayoutEffect` in the browser, `useEffect` on the server * |
|
|
53
|
+
| `@journ/lift/use-keybindings` | `useKeybindings(map, target, options)` | Keyboard shortcuts and key sequences * |
|
|
54
|
+
| `@journ/lift/use-local-storage` | `useLocalStorage`, `useSessionStorage` | State synced with web storage * |
|
|
55
|
+
| `@journ/lift/use-logger` | `useLogger(name, props)` | Logs mount, update and unmount |
|
|
56
|
+
| `@journ/lift/use-long-press` | `useLongPress(onLongPress, disabled)` | Pointer handlers that fire after a 500 ms hold |
|
|
57
|
+
| `@journ/lift/use-map` | `useMap(entries)` | A `Map` that re-renders on change |
|
|
58
|
+
| `@journ/lift/use-measure` | `useMeasure()` | Element width and height * |
|
|
59
|
+
| `@journ/lift/use-media-query` | `useMediaQuery(query)`, `useBreakpoint(name)`, `breakpoints` | A media query match * |
|
|
60
|
+
| `@journ/lift/use-mouse` | `useMouse(options)`, `useMousePosition()` | Pointer position, relative to an element or the window * |
|
|
61
|
+
| `@journ/lift/use-network` | `useNetwork()` | Online state and connection details * |
|
|
62
|
+
| `@journ/lift/use-page-leave` | `usePageLeave(fn)` | Calls `fn` when the pointer leaves the page * |
|
|
63
|
+
| `@journ/lift/use-pagination` | `usePagination(options)`, `DOTS` | Page ranges with siblings and boundaries |
|
|
64
|
+
| `@journ/lift/use-reduced-motion` | `useReducedMotion()` | The `prefers-reduced-motion` media query * |
|
|
65
|
+
| `@journ/lift/use-scroll-direction` | `useScrollDirection()` | `up`, `down` or `unknown` * |
|
|
66
|
+
| `@journ/lift/use-uncontrolled` | `useUncontrolled(options)` | Controlled or uncontrolled value with a change handler |
|
|
67
|
+
| `@journ/lift/use-validated-state` | `useValidatedState(initial, validate)` | A value with its validity and last valid value |
|
|
68
|
+
| `@journ/lift/use-window-event` | `useWindowEvent(type, listener, options)` | A window event listener that never goes stale * |
|
|
69
|
+
| `@journ/lift/use-window-scroll` | `useWindowScroll()` | Window scroll position and a smooth `scrollTo` * |
|
|
70
|
+
| `@journ/lift/required-context` | `createRequiredContext(owner)` | a context whose hook throws outside it |
|
|
71
|
+
| `@journ/lift/prefers-reduced-motion` | `prefersReducedMotion()` | |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/lib/logger.ts
|
|
2
|
+
const isProduction = () => {
|
|
3
|
+
const env = import.meta.env;
|
|
4
|
+
if (typeof env?.PROD === "boolean") return env.PROD;
|
|
5
|
+
return globalThis.process?.env?.NODE_ENV === "production";
|
|
6
|
+
};
|
|
7
|
+
const createLogger = () => {
|
|
8
|
+
const shouldLog = (level) => {
|
|
9
|
+
if (isProduction()) return level === "error";
|
|
10
|
+
return true;
|
|
11
|
+
};
|
|
12
|
+
const error = (...args) => {
|
|
13
|
+
if (shouldLog("error")) console.error(...args);
|
|
14
|
+
};
|
|
15
|
+
const warn = (...args) => {
|
|
16
|
+
if (shouldLog("warn")) console.warn(...args);
|
|
17
|
+
};
|
|
18
|
+
const info = (...args) => {
|
|
19
|
+
if (shouldLog("info")) console.info(...args);
|
|
20
|
+
};
|
|
21
|
+
const log = (...args) => {
|
|
22
|
+
if (shouldLog("log")) console.log(...args);
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
error,
|
|
26
|
+
warn,
|
|
27
|
+
info,
|
|
28
|
+
log
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const logger = createLogger();
|
|
32
|
+
//#endregion
|
|
33
|
+
export { logger as t };
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
//#region src/prefers-reduced-motion.ts
|
|
2
|
-
|
|
3
|
-
return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
|
|
4
|
-
}
|
|
2
|
+
const prefersReducedMotion = () => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
|
|
5
3
|
//#endregion
|
|
6
4
|
export { prefersReducedMotion };
|
|
@@ -4,5 +4,6 @@ import { Context } from "react";
|
|
|
4
4
|
* A context that parts must be rendered inside of. The hook throws a clear error
|
|
5
5
|
* naming the owner, so a part used on its own fails loudly instead of rendering wrong.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
//#endregion
|
|
7
|
+
declare const createRequiredContext: <T>(owner: string) => readonly [Context<T | null>, () => T];
|
|
8
|
+
//#endregion
|
|
9
|
+
export { createRequiredContext };
|
package/dist/required-context.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createContext, use } from "react";
|
|
|
4
4
|
* A context that parts must be rendered inside of. The hook throws a clear error
|
|
5
5
|
* naming the owner, so a part used on its own fails loudly instead of rendering wrong.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
const createRequiredContext = (owner) => {
|
|
8
8
|
const RequiredContext = createContext(null);
|
|
9
9
|
const useRequired = () => {
|
|
10
10
|
const value = use(RequiredContext);
|
|
@@ -12,6 +12,6 @@ function createRequiredContext(owner) {
|
|
|
12
12
|
return value;
|
|
13
13
|
};
|
|
14
14
|
return [RequiredContext, useRequired];
|
|
15
|
-
}
|
|
15
|
+
};
|
|
16
16
|
//#endregion
|
|
17
17
|
export { createRequiredContext };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/lib/try-catch.ts
|
|
2
|
+
const tryCatch = async (promise) => {
|
|
3
|
+
try {
|
|
4
|
+
return {
|
|
5
|
+
success: true,
|
|
6
|
+
data: await promise
|
|
7
|
+
};
|
|
8
|
+
} catch (error) {
|
|
9
|
+
return {
|
|
10
|
+
success: false,
|
|
11
|
+
error
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const tryCatchSync = (fn) => {
|
|
16
|
+
try {
|
|
17
|
+
return {
|
|
18
|
+
success: true,
|
|
19
|
+
data: fn()
|
|
20
|
+
};
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
error
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { tryCatchSync as n, tryCatch as t };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/use-battery.d.ts
|
|
2
|
+
type BatteryState = {
|
|
3
|
+
supported: boolean;
|
|
4
|
+
loading: boolean;
|
|
5
|
+
level: number | null;
|
|
6
|
+
charging: boolean | null;
|
|
7
|
+
chargingTime: number | null;
|
|
8
|
+
dischargingTime: number | null;
|
|
9
|
+
};
|
|
10
|
+
declare const useBattery: () => BatteryState;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { type BatteryState, useBattery };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { t as tryCatch } from "./try-catch-D4d3N9_W.js";
|
|
2
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
3
|
+
//#region src/use-battery.ts
|
|
4
|
+
const BATTERY_EVENTS = [
|
|
5
|
+
"levelchange",
|
|
6
|
+
"chargingchange",
|
|
7
|
+
"chargingtimechange",
|
|
8
|
+
"dischargingtimechange"
|
|
9
|
+
];
|
|
10
|
+
const INITIAL_STATE = {
|
|
11
|
+
supported: true,
|
|
12
|
+
loading: true,
|
|
13
|
+
level: null,
|
|
14
|
+
charging: null,
|
|
15
|
+
chargingTime: null,
|
|
16
|
+
dischargingTime: null
|
|
17
|
+
};
|
|
18
|
+
const UNSUPPORTED_STATE = {
|
|
19
|
+
...INITIAL_STATE,
|
|
20
|
+
supported: false,
|
|
21
|
+
loading: false
|
|
22
|
+
};
|
|
23
|
+
const readBattery = (battery) => ({
|
|
24
|
+
supported: true,
|
|
25
|
+
loading: false,
|
|
26
|
+
level: battery.level,
|
|
27
|
+
charging: battery.charging,
|
|
28
|
+
chargingTime: battery.chargingTime,
|
|
29
|
+
dischargingTime: battery.dischargingTime
|
|
30
|
+
});
|
|
31
|
+
const subscribeNever = () => () => {};
|
|
32
|
+
const hasBatteryApi = () => "getBattery" in navigator;
|
|
33
|
+
const useBattery = () => {
|
|
34
|
+
const supported = useSyncExternalStore(subscribeNever, hasBatteryApi, () => true);
|
|
35
|
+
const [state, setState] = useState(INITIAL_STATE);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const nav = navigator;
|
|
38
|
+
if (!nav.getBattery) return;
|
|
39
|
+
const getBattery = nav.getBattery.bind(nav);
|
|
40
|
+
let cancelled = false;
|
|
41
|
+
let detach = () => {};
|
|
42
|
+
tryCatch(new Promise((resolve) => resolve(getBattery()))).then((result) => {
|
|
43
|
+
if (cancelled) return;
|
|
44
|
+
if (!result.success) {
|
|
45
|
+
setState(UNSUPPORTED_STATE);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const battery = result.data;
|
|
49
|
+
const update = () => setState(readBattery(battery));
|
|
50
|
+
update();
|
|
51
|
+
for (const event of BATTERY_EVENTS) battery.addEventListener(event, update);
|
|
52
|
+
detach = () => {
|
|
53
|
+
for (const event of BATTERY_EVENTS) battery.removeEventListener(event, update);
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
return () => {
|
|
57
|
+
cancelled = true;
|
|
58
|
+
detach();
|
|
59
|
+
};
|
|
60
|
+
}, []);
|
|
61
|
+
return supported ? state : UNSUPPORTED_STATE;
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { useBattery };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/use-browser.d.ts
|
|
2
|
+
type Browser = "undetermined" | "chrome" | "firefox" | "safari" | "edge" | "opera" | "ie" | "other";
|
|
3
|
+
declare const getBrowserVersion: () => string | null;
|
|
4
|
+
declare const useBrowser: () => Browser;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { type Browser, getBrowserVersion, useBrowser };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { t as isServerSide } from "./env-C0PNnbwt.js";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
//#region src/use-browser.ts
|
|
4
|
+
const matchers = [
|
|
5
|
+
{
|
|
6
|
+
browser: "edge",
|
|
7
|
+
detect: /Edge\/([0-9]+)/i,
|
|
8
|
+
version: /Edge\/([0-9]+)/i
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
browser: "chrome",
|
|
12
|
+
detect: /(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9]+)/i,
|
|
13
|
+
version: /Chrom(?:e|ium)\/([0-9]+)/i
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
browser: "firefox",
|
|
17
|
+
detect: /Firefox\/([0-9]+)/i,
|
|
18
|
+
version: /Firefox\/([0-9]+)/i
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
browser: "safari",
|
|
22
|
+
detect: /^((?!chrome|android).)*safari/i,
|
|
23
|
+
version: /Version\/([0-9]+)/i
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
browser: "opera",
|
|
27
|
+
detect: /(OPR|Opera)\/([0-9]+)/i,
|
|
28
|
+
version: /(?:OPR|Opera)\/([0-9]+)/i
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
browser: "ie",
|
|
32
|
+
detect: /MSIE|Trident/i,
|
|
33
|
+
version: /(?:MSIE |rv:)([0-9]+)/i
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
const detectBrowser = (userAgent) => matchers.find(({ detect }) => detect.test(userAgent));
|
|
37
|
+
const getBrowser = () => {
|
|
38
|
+
if (isServerSide()) return "undetermined";
|
|
39
|
+
return detectBrowser(window.navigator.userAgent)?.browser ?? "other";
|
|
40
|
+
};
|
|
41
|
+
const getBrowserVersion = () => {
|
|
42
|
+
if (isServerSide()) return null;
|
|
43
|
+
const { userAgent } = window.navigator;
|
|
44
|
+
const matcher = detectBrowser(userAgent);
|
|
45
|
+
return matcher ? userAgent.match(matcher.version)?.[1] ?? null : null;
|
|
46
|
+
};
|
|
47
|
+
const subscribe = () => () => {};
|
|
48
|
+
const getServerSnapshot = () => "undetermined";
|
|
49
|
+
const useBrowser = () => useSyncExternalStore(subscribe, getBrowser, getServerSnapshot);
|
|
50
|
+
//#endregion
|
|
51
|
+
export { getBrowserVersion, useBrowser };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
//#region src/use-callback-ref.ts
|
|
3
|
+
const useCallbackRef = (callback) => {
|
|
4
|
+
const callbackRef = useRef(callback);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
callbackRef.current = callback;
|
|
7
|
+
});
|
|
8
|
+
return useMemo(() => ((...args) => callbackRef.current?.(...args)), []);
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { useCallbackRef };
|
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
* State that the caller may own. `value` undefined means uncontrolled: the hook
|
|
4
4
|
* keeps the state and still reports every change through `onChange`.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
//#endregion
|
|
6
|
+
declare const useControllableState: <T>(value: T | null | undefined, defaultValue: T | null, onChange?: (value: T) => void) => readonly [T | null, (next: T) => void];
|
|
7
|
+
//#endregion
|
|
8
|
+
export { useControllableState };
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
+
import { useCallbackRef } from "./use-callback-ref.js";
|
|
1
2
|
import { useCallback, useState } from "react";
|
|
2
3
|
//#region src/use-controllable-state.ts
|
|
3
4
|
/**
|
|
4
5
|
* State that the caller may own. `value` undefined means uncontrolled: the hook
|
|
5
6
|
* keeps the state and still reports every change through `onChange`.
|
|
6
7
|
*/
|
|
7
|
-
|
|
8
|
+
const useControllableState = (value, defaultValue, onChange) => {
|
|
8
9
|
const [uncontrolled, setUncontrolled] = useState(defaultValue);
|
|
9
10
|
const controlled = value !== void 0;
|
|
11
|
+
const reportChange = useCallbackRef(onChange);
|
|
10
12
|
const setValue = useCallback((next) => {
|
|
11
13
|
if (!controlled) setUncontrolled(next);
|
|
12
|
-
|
|
13
|
-
}, [controlled,
|
|
14
|
+
reportChange(next);
|
|
15
|
+
}, [controlled, reportChange]);
|
|
14
16
|
return [controlled ? value : uncontrolled, setValue];
|
|
15
|
-
}
|
|
17
|
+
};
|
|
16
18
|
//#endregion
|
|
17
19
|
export { useControllableState };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SetStateAction } from "react";
|
|
2
|
+
//#region src/use-debounce-state.d.ts
|
|
3
|
+
type UseDebouncedStateOptions = {
|
|
4
|
+
leading?: boolean;
|
|
5
|
+
};
|
|
6
|
+
type UseDebouncedStateReturnValue<T> = [T, (newValue: SetStateAction<T>) => void];
|
|
7
|
+
declare const useDebouncedState: <T = unknown>(defaultValue: T, wait: number, { leading }?: UseDebouncedStateOptions) => UseDebouncedStateReturnValue<T>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { useDebouncedState };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
//#region src/use-debounce-state.ts
|
|
3
|
+
const useDebouncedState = (defaultValue, wait, { leading = false } = {}) => {
|
|
4
|
+
const [value, setValue] = useState(defaultValue);
|
|
5
|
+
const timeoutRef = useRef(void 0);
|
|
6
|
+
const leadingRef = useRef(true);
|
|
7
|
+
const cancel = useCallback(() => window.clearTimeout(timeoutRef.current), []);
|
|
8
|
+
useEffect(() => cancel, [cancel]);
|
|
9
|
+
return [value, useCallback((newValue) => {
|
|
10
|
+
cancel();
|
|
11
|
+
if (leadingRef.current && leading) {
|
|
12
|
+
setValue(newValue);
|
|
13
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
14
|
+
leadingRef.current = true;
|
|
15
|
+
}, wait);
|
|
16
|
+
} else timeoutRef.current = window.setTimeout(() => {
|
|
17
|
+
leadingRef.current = true;
|
|
18
|
+
setValue(newValue);
|
|
19
|
+
}, wait);
|
|
20
|
+
leadingRef.current = false;
|
|
21
|
+
}, [
|
|
22
|
+
leading,
|
|
23
|
+
cancel,
|
|
24
|
+
wait
|
|
25
|
+
])];
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
export { useDebouncedState };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { t as ANY } from "./types-p0KNRcj3.js";
|
|
2
|
+
//#region src/use-debounced-callback.d.ts
|
|
3
|
+
type UseDebouncedCallbackOptions = {
|
|
4
|
+
delay: number;
|
|
5
|
+
flushOnUnmount?: boolean;
|
|
6
|
+
leading?: boolean;
|
|
7
|
+
maxWait?: number;
|
|
8
|
+
};
|
|
9
|
+
type UseDebouncedCallbackReturnValue<T extends (...args: ANY[]) => ANY> = ((...args: Parameters<T>) => void) & {
|
|
10
|
+
flush: () => void;
|
|
11
|
+
cancel: () => void;
|
|
12
|
+
isPending: () => boolean;
|
|
13
|
+
};
|
|
14
|
+
declare const useDebouncedCallback: <T extends (...args: ANY[]) => ANY>(callback: T, options: number | UseDebouncedCallbackOptions) => UseDebouncedCallbackReturnValue<T>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { type UseDebouncedCallbackOptions, type UseDebouncedCallbackReturnValue, useDebouncedCallback };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useCallbackRef } from "./use-callback-ref.js";
|
|
2
|
+
import { useEffect, useMemo } from "react";
|
|
3
|
+
//#region src/use-debounced-callback.ts
|
|
4
|
+
const createDebounced = (run, { delay, leading, maxWait }) => {
|
|
5
|
+
let debounceTimer;
|
|
6
|
+
let maxWaitTimer;
|
|
7
|
+
let latestArgs;
|
|
8
|
+
let isFirstCall = true;
|
|
9
|
+
let pending = false;
|
|
10
|
+
const cancel = () => {
|
|
11
|
+
window.clearTimeout(debounceTimer);
|
|
12
|
+
window.clearTimeout(maxWaitTimer);
|
|
13
|
+
debounceTimer = void 0;
|
|
14
|
+
maxWaitTimer = void 0;
|
|
15
|
+
isFirstCall = true;
|
|
16
|
+
pending = false;
|
|
17
|
+
};
|
|
18
|
+
const flush = () => {
|
|
19
|
+
if (pending && debounceTimer !== void 0 && latestArgs) {
|
|
20
|
+
const args = latestArgs;
|
|
21
|
+
cancel();
|
|
22
|
+
run(...args);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const startMaxWait = () => {
|
|
26
|
+
if (maxWait !== void 0 && maxWaitTimer === void 0) maxWaitTimer = window.setTimeout(flush, maxWait);
|
|
27
|
+
};
|
|
28
|
+
const debounced = (...args) => {
|
|
29
|
+
window.clearTimeout(debounceTimer);
|
|
30
|
+
latestArgs = args;
|
|
31
|
+
const isLeadingCall = leading && isFirstCall;
|
|
32
|
+
isFirstCall = false;
|
|
33
|
+
if (isLeadingCall) {
|
|
34
|
+
run(...args);
|
|
35
|
+
debounceTimer = window.setTimeout(cancel, delay);
|
|
36
|
+
} else {
|
|
37
|
+
pending = true;
|
|
38
|
+
debounceTimer = window.setTimeout(leading ? cancel : flush, delay);
|
|
39
|
+
}
|
|
40
|
+
startMaxWait();
|
|
41
|
+
};
|
|
42
|
+
return Object.assign(debounced, {
|
|
43
|
+
flush,
|
|
44
|
+
cancel,
|
|
45
|
+
isPending: () => pending
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const useDebouncedCallback = (callback, options) => {
|
|
49
|
+
const { delay, flushOnUnmount = false, leading = false, maxWait } = typeof options === "number" ? { delay: options } : options;
|
|
50
|
+
const handleCallback = useCallbackRef(callback);
|
|
51
|
+
const debounced = useMemo(() => createDebounced(handleCallback, {
|
|
52
|
+
delay,
|
|
53
|
+
leading,
|
|
54
|
+
maxWait
|
|
55
|
+
}), [
|
|
56
|
+
handleCallback,
|
|
57
|
+
delay,
|
|
58
|
+
leading,
|
|
59
|
+
maxWait
|
|
60
|
+
]);
|
|
61
|
+
useEffect(() => () => {
|
|
62
|
+
if (flushOnUnmount) debounced.flush();
|
|
63
|
+
else debounced.cancel();
|
|
64
|
+
}, [debounced, flushOnUnmount]);
|
|
65
|
+
return debounced;
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
export { useDebouncedCallback };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import { debounce } from "@journ/fitter-happier";
|
|
2
|
+
import { debounce } from "@journ/fitter-happier/debounce";
|
|
3
3
|
//#region src/use-debounced-value.ts
|
|
4
|
-
|
|
4
|
+
const useDebouncedValue = (value, delay) => {
|
|
5
5
|
const [debounced, setDebounced] = useState(value);
|
|
6
6
|
useEffect(() => {
|
|
7
7
|
const update = debounce(setDebounced, delay);
|
|
@@ -9,6 +9,6 @@ function useDebouncedValue(value, delay) {
|
|
|
9
9
|
return update.cancel;
|
|
10
10
|
}, [value, delay]);
|
|
11
11
|
return debounced;
|
|
12
|
-
}
|
|
12
|
+
};
|
|
13
13
|
//#endregion
|
|
14
14
|
export { useDebouncedValue };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
//#region src/use-did-update.ts
|
|
3
|
+
const dependenciesChanged = (previous, next) => previous.length !== next.length || next.some((dep, index) => !Object.is(dep, previous[index]));
|
|
4
|
+
const useDidUpdate = (fn, dependencies) => {
|
|
5
|
+
const mounted = useRef(false);
|
|
6
|
+
const previousRender = useRef(null);
|
|
7
|
+
const previousDependencies = useRef(void 0);
|
|
8
|
+
const render = {};
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const isSameRender = previousRender.current === render;
|
|
11
|
+
const previous = previousDependencies.current;
|
|
12
|
+
previousRender.current = render;
|
|
13
|
+
previousDependencies.current = dependencies;
|
|
14
|
+
if (!mounted.current) {
|
|
15
|
+
mounted.current = true;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (isSameRender) return;
|
|
19
|
+
if (dependencies && previous && !dependenciesChanged(previous, dependencies)) return;
|
|
20
|
+
return fn();
|
|
21
|
+
}, dependencies);
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { useDidUpdate };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/use-disclosure.d.ts
|
|
2
|
+
type UseDisclosureOptions = {
|
|
3
|
+
onOpen?: () => void;
|
|
4
|
+
onClose?: () => void;
|
|
5
|
+
};
|
|
6
|
+
type UseDisclosureHandlers = {
|
|
7
|
+
set: (value: boolean) => void;
|
|
8
|
+
open: () => void;
|
|
9
|
+
close: () => void;
|
|
10
|
+
toggle: () => void;
|
|
11
|
+
};
|
|
12
|
+
type UseDisclosureReturnValue = [boolean, UseDisclosureHandlers];
|
|
13
|
+
declare const useDisclosure: (initialState?: boolean, { onOpen, onClose }?: UseDisclosureOptions) => UseDisclosureReturnValue;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { type UseDisclosureHandlers, type UseDisclosureOptions, type UseDisclosureReturnValue, useDisclosure };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useDidUpdate } from "./use-did-update.js";
|
|
2
|
+
import { useCallback, useEffectEvent, useState } from "react";
|
|
3
|
+
//#region src/use-disclosure.ts
|
|
4
|
+
const useDisclosure = (initialState = false, { onOpen, onClose } = {}) => {
|
|
5
|
+
const [opened, setOpened] = useState(initialState);
|
|
6
|
+
const notify = useEffectEvent((isOpen) => isOpen ? onOpen?.() : onClose?.());
|
|
7
|
+
useDidUpdate(() => notify(opened), [opened]);
|
|
8
|
+
return [opened, {
|
|
9
|
+
open: useCallback(() => setOpened(true), []),
|
|
10
|
+
close: useCallback(() => setOpened(false), []),
|
|
11
|
+
toggle: useCallback(() => setOpened((current) => !current), []),
|
|
12
|
+
set: setOpened
|
|
13
|
+
}];
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { useDisclosure };
|
package/dist/use-draft.d.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
//#region src/use-draft.d.ts
|
|
2
|
-
|
|
2
|
+
type UseDraftOptions<T> = {
|
|
3
3
|
value: T;
|
|
4
4
|
format: (value: T) => string;
|
|
5
5
|
/** Return undefined to reject the text, which keeps the old value. */
|
|
6
6
|
parse: (text: string) => T | undefined;
|
|
7
7
|
onCommit: (value: T) => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
8
|
+
};
|
|
9
|
+
type Draft = {
|
|
10
10
|
text: string;
|
|
11
11
|
edit: (text: string) => void;
|
|
12
12
|
discard: () => void;
|
|
13
13
|
commit: () => void;
|
|
14
|
-
}
|
|
14
|
+
};
|
|
15
15
|
/**
|
|
16
16
|
* Text that is typed freely and only turns into a value on commit. While not
|
|
17
17
|
* editing it shows `format(value)`.
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
//#endregion
|
|
19
|
+
declare const useDraft: <T>({ value, format, parse, onCommit }: UseDraftOptions<T>) => Draft;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { type Draft, type UseDraftOptions, useDraft };
|
package/dist/use-draft.js
CHANGED
|
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
* Text that is typed freely and only turns into a value on commit. While not
|
|
5
5
|
* editing it shows `format(value)`.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
const useDraft = ({ value, format, parse, onCommit }) => {
|
|
8
8
|
const [draft, setDraft] = useState(null);
|
|
9
9
|
return {
|
|
10
10
|
text: draft ?? format(value),
|
|
@@ -16,6 +16,6 @@ function useDraft({ value, format, parse, onCommit }) {
|
|
|
16
16
|
setDraft(null);
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
|
-
}
|
|
19
|
+
};
|
|
20
20
|
//#endregion
|
|
21
21
|
export { useDraft };
|