@-reddacted-/react-hooks 0.1.0 → 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/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/useAsync.d.ts +18 -0
- package/dist/useAsync.d.ts.map +1 -0
- package/dist/useAsync.js +39 -0
- package/dist/useClickOutside.d.ts +9 -0
- package/dist/useClickOutside.d.ts.map +1 -0
- package/dist/useClickOutside.js +22 -0
- package/dist/useDocumentTitle.d.ts +2 -0
- package/dist/useDocumentTitle.d.ts.map +1 -0
- package/dist/useDocumentTitle.js +8 -0
- package/dist/useElementSize.d.ts +13 -0
- package/dist/useElementSize.d.ts.map +1 -0
- package/dist/useElementSize.js +26 -0
- package/dist/useEscapeKey.d.ts +2 -0
- package/dist/useEscapeKey.d.ts.map +1 -0
- package/dist/useEscapeKey.js +13 -0
- package/package.json +8 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export { useFullscreen } from "./useFullscreen";
|
|
2
|
-
export { useInstallPrompt } from "./useInstallPrompt";
|
|
3
|
-
export
|
|
4
|
-
export {
|
|
1
|
+
export { useFullscreen } from "./useFullscreen.js";
|
|
2
|
+
export { useInstallPrompt, type UseInstallPromptOptions } from "./useInstallPrompt.js";
|
|
3
|
+
export { useValueChange } from "./useValueChange.js";
|
|
4
|
+
export { useAsync, type AsyncStatus, type AsyncResult } from "./useAsync.js";
|
|
5
|
+
export { useElementSize, type ElementSize } from "./useElementSize.js";
|
|
6
|
+
export { useClickOutside } from "./useClickOutside.js";
|
|
7
|
+
export { useEscapeKey } from "./useEscapeKey.js";
|
|
8
|
+
export { useDocumentTitle } from "./useDocumentTitle.js";
|
|
5
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACtF,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;AAC5E,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
export { useFullscreen } from "./useFullscreen";
|
|
2
|
-
export { useInstallPrompt } from "./useInstallPrompt";
|
|
3
|
-
export { useValueChange } from "./useValueChange";
|
|
1
|
+
export { useFullscreen } from "./useFullscreen.js";
|
|
2
|
+
export { useInstallPrompt } from "./useInstallPrompt.js";
|
|
3
|
+
export { useValueChange } from "./useValueChange.js";
|
|
4
|
+
export { useAsync } from "./useAsync.js";
|
|
5
|
+
export { useElementSize } from "./useElementSize.js";
|
|
6
|
+
export { useClickOutside } from "./useClickOutside.js";
|
|
7
|
+
export { useEscapeKey } from "./useEscapeKey.js";
|
|
8
|
+
export { useDocumentTitle } from "./useDocumentTitle.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DependencyList } from "react";
|
|
2
|
+
export type AsyncStatus = "loading" | "ready" | "error";
|
|
3
|
+
export type AsyncResult<T> = {
|
|
4
|
+
data: T | null;
|
|
5
|
+
status: AsyncStatus;
|
|
6
|
+
error: unknown;
|
|
7
|
+
/** Runs the task again. Wire it to a retry button. */
|
|
8
|
+
retry: () => void;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Runs an abortable async task and tracks its three states.
|
|
12
|
+
*
|
|
13
|
+
* The task receives an AbortSignal that fires when the dependencies change or the
|
|
14
|
+
* component unmounts, so a stale response can never overwrite a fresh one. An aborted
|
|
15
|
+
* task is not an error and leaves the state untouched.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useAsync<T>(task: (signal: AbortSignal) => Promise<T>, deps?: DependencyList): AsyncResult<T>;
|
|
18
|
+
//# sourceMappingURL=useAsync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAsync.d.ts","sourceRoot":"","sources":["../src/useAsync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAE3C,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAEvD,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,IAAI,EAAE,CAAC,GAAG,IAAI,CAAA;IACd,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,sDAAsD;IACtD,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB,CAAA;AAQD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,GAAE,cAAmB,GAAG,WAAW,CAAC,CAAC,CAAC,CAmChH"}
|
package/dist/useAsync.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
function isAbort(cause) {
|
|
3
|
+
return cause instanceof Error && cause.name === "AbortError";
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Runs an abortable async task and tracks its three states.
|
|
7
|
+
*
|
|
8
|
+
* The task receives an AbortSignal that fires when the dependencies change or the
|
|
9
|
+
* component unmounts, so a stale response can never overwrite a fresh one. An aborted
|
|
10
|
+
* task is not an error and leaves the state untouched.
|
|
11
|
+
*/
|
|
12
|
+
export function useAsync(task, deps = []) {
|
|
13
|
+
const [state, setState] = useState({ data: null, status: "loading", error: null });
|
|
14
|
+
const [attempt, setAttempt] = useState(0);
|
|
15
|
+
// La tâche est le plus souvent une closure recréée à chaque rendu : la garder dans un
|
|
16
|
+
// ref laisse les dépendances déclarées seules maîtresses du redéclenchement.
|
|
17
|
+
const taskRef = useRef(task);
|
|
18
|
+
taskRef.current = task;
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
let active = true;
|
|
22
|
+
setState(current => ({ ...current, status: "loading" }));
|
|
23
|
+
taskRef.current(controller.signal).then(data => {
|
|
24
|
+
if (active)
|
|
25
|
+
setState({ data, status: "ready", error: null });
|
|
26
|
+
}, error => {
|
|
27
|
+
if (!active || controller.signal.aborted || isAbort(error))
|
|
28
|
+
return;
|
|
29
|
+
setState({ data: null, status: "error", error });
|
|
30
|
+
});
|
|
31
|
+
return () => {
|
|
32
|
+
active = false;
|
|
33
|
+
controller.abort();
|
|
34
|
+
};
|
|
35
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
|
+
}, [...deps, attempt]);
|
|
37
|
+
const retry = useCallback(() => setAttempt(n => n + 1), []);
|
|
38
|
+
return { ...state, retry };
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Calls the handler on a pointer press outside every given element.
|
|
4
|
+
*
|
|
5
|
+
* Listens on mousedown rather than click: a press that starts outside and ends inside
|
|
6
|
+
* should still dismiss, and the handler runs before the target rerenders.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useClickOutside(refs: Array<RefObject<HTMLElement | null>>, handler: () => void, enabled?: boolean): void;
|
|
9
|
+
//# sourceMappingURL=useClickOutside.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useClickOutside.d.ts","sourceRoot":"","sources":["../src/useClickOutside.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,EAC1C,OAAO,EAAE,MAAM,IAAI,EACnB,OAAO,UAAO,GACb,IAAI,CAcN"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Calls the handler on a pointer press outside every given element.
|
|
4
|
+
*
|
|
5
|
+
* Listens on mousedown rather than click: a press that starts outside and ends inside
|
|
6
|
+
* should still dismiss, and the handler runs before the target rerenders.
|
|
7
|
+
*/
|
|
8
|
+
export function useClickOutside(refs, handler, enabled = true) {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!enabled)
|
|
11
|
+
return;
|
|
12
|
+
const onMouseDown = (event) => {
|
|
13
|
+
const target = event.target;
|
|
14
|
+
if (refs.some(ref => ref.current?.contains(target)))
|
|
15
|
+
return;
|
|
16
|
+
handler();
|
|
17
|
+
};
|
|
18
|
+
document.addEventListener("mousedown", onMouseDown);
|
|
19
|
+
return () => document.removeEventListener("mousedown", onMouseDown);
|
|
20
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
|
+
}, [enabled, handler, ...refs]);
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDocumentTitle.d.ts","sourceRoot":"","sources":["../src/useDocumentTitle.ts"],"names":[],"mappings":"AAIA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,QAI9F"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
// Without this every page shares the title of index.html in the tab, the history
|
|
3
|
+
// and the search results.
|
|
4
|
+
export function useDocumentTitle(title, siteName, fallback) {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
document.title = title ? `${title} — ${siteName}` : (fallback ?? siteName);
|
|
7
|
+
}, [title, siteName, fallback]);
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
export type ElementSize = {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Observed size of an element. Null until the first measure.
|
|
8
|
+
*
|
|
9
|
+
* useLayoutEffect rather than useEffect: the first measure lands before paint, so a
|
|
10
|
+
* layout that depends on it never flashes at a default size before settling.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useElementSize(ref: RefObject<HTMLElement | null>): ElementSize | null;
|
|
13
|
+
//# sourceMappingURL=useElementSize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useElementSize.d.ts","sourceRoot":"","sources":["../src/useElementSize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE3D;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,WAAW,GAAG,IAAI,CAsBrF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Observed size of an element. Null until the first measure.
|
|
4
|
+
*
|
|
5
|
+
* useLayoutEffect rather than useEffect: the first measure lands before paint, so a
|
|
6
|
+
* layout that depends on it never flashes at a default size before settling.
|
|
7
|
+
*/
|
|
8
|
+
export function useElementSize(ref) {
|
|
9
|
+
const [size, setSize] = useState(null);
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
const element = ref.current;
|
|
12
|
+
if (!element)
|
|
13
|
+
return;
|
|
14
|
+
const measure = ({ width, height }) => setSize({ width, height });
|
|
15
|
+
const rect = element.getBoundingClientRect();
|
|
16
|
+
measure({ width: rect.width, height: rect.height });
|
|
17
|
+
const observer = new ResizeObserver(entries => {
|
|
18
|
+
const rect = entries[0]?.contentRect;
|
|
19
|
+
if (rect)
|
|
20
|
+
measure({ width: rect.width, height: rect.height });
|
|
21
|
+
});
|
|
22
|
+
observer.observe(element);
|
|
23
|
+
return () => observer.disconnect();
|
|
24
|
+
}, [ref]);
|
|
25
|
+
return size;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEscapeKey.d.ts","sourceRoot":"","sources":["../src/useEscapeKey.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,OAAO,UAAO,GAAG,IAAI,CAWtE"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
export function useEscapeKey(handler, enabled = true) {
|
|
3
|
+
useEffect(() => {
|
|
4
|
+
if (!enabled)
|
|
5
|
+
return;
|
|
6
|
+
const onKeyDown = (event) => {
|
|
7
|
+
if (event.key === "Escape")
|
|
8
|
+
handler();
|
|
9
|
+
};
|
|
10
|
+
document.addEventListener("keydown", onKeyDown);
|
|
11
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
12
|
+
}, [enabled, handler]);
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@-reddacted-/react-hooks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "useFullscreen, useInstallPrompt, useValueChange
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "React hooks: useAsync, useElementSize, useClickOutside, useEscapeKey, useDocumentTitle, useFullscreen, useInstallPrompt, useValueChange",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -11,15 +11,18 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
15
17
|
"scripts": {
|
|
16
|
-
"build": "tsc"
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
17
20
|
},
|
|
18
21
|
"peerDependencies": {
|
|
19
22
|
"react": ">=18"
|
|
20
23
|
},
|
|
21
24
|
"devDependencies": {
|
|
22
|
-
"@types/react": "^
|
|
25
|
+
"@types/react": "^18.3.1",
|
|
23
26
|
"typescript": "~5.8.3"
|
|
24
27
|
}
|
|
25
28
|
}
|