@develemit/web-hooks 0.0.0-stage → 0.1.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/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +83 -2
- package/dist/clipboard.cjs +89 -0
- package/dist/clipboard.d.cts +17 -0
- package/dist/clipboard.d.ts +17 -0
- package/dist/clipboard.js +62 -0
- package/dist/drawer-swipe.cjs +149 -0
- package/dist/drawer-swipe.d.cts +26 -0
- package/dist/drawer-swipe.d.ts +26 -0
- package/dist/drawer-swipe.js +114 -0
- package/dist/escape.cjs +47 -0
- package/dist/escape.d.cts +3 -0
- package/dist/escape.d.ts +3 -0
- package/dist/escape.js +21 -0
- package/dist/focus-trap.cjs +108 -0
- package/dist/focus-trap.d.cts +17 -0
- package/dist/focus-trap.d.ts +17 -0
- package/dist/focus-trap.js +79 -0
- package/dist/index.cjs +308 -0
- package/dist/index.d.cts +60 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +265 -0
- package/package.json +73 -3
package/dist/escape.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/escape/use-escape.ts
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
function useEscape(onEscape, active = true) {
|
|
6
|
+
const handlerRef = useRef(onEscape);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
handlerRef.current = onEscape;
|
|
9
|
+
});
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!active) return;
|
|
12
|
+
function onKeyDown(event) {
|
|
13
|
+
if (event.key === "Escape") handlerRef.current();
|
|
14
|
+
}
|
|
15
|
+
document.addEventListener("keydown", onKeyDown);
|
|
16
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
17
|
+
}, [active]);
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
useEscape
|
|
21
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/focus-trap/index.ts
|
|
22
|
+
var focus_trap_exports = {};
|
|
23
|
+
__export(focus_trap_exports, {
|
|
24
|
+
FOCUSABLE_SELECTOR: () => FOCUSABLE_SELECTOR,
|
|
25
|
+
focusableWithin: () => focusableWithin,
|
|
26
|
+
nextFocusTarget: () => nextFocusTarget,
|
|
27
|
+
useFocusTrap: () => useFocusTrap
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(focus_trap_exports);
|
|
30
|
+
|
|
31
|
+
// src/focus-trap/focusables.ts
|
|
32
|
+
var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
33
|
+
function focusableWithin(container) {
|
|
34
|
+
return Array.from(
|
|
35
|
+
container.querySelectorAll(FOCUSABLE_SELECTOR)
|
|
36
|
+
).filter((el) => !el.closest("[hidden], [inert]"));
|
|
37
|
+
}
|
|
38
|
+
function nextFocusTarget(items, current, shiftKey, container) {
|
|
39
|
+
const first = items[0];
|
|
40
|
+
const last = items[items.length - 1];
|
|
41
|
+
if (!first || !last) return null;
|
|
42
|
+
if (!current || !container.contains(current)) return shiftKey ? last : first;
|
|
43
|
+
if (shiftKey && (current === first || current === container)) return last;
|
|
44
|
+
if (!shiftKey && current === last) return first;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/focus-trap/use-focus-trap.ts
|
|
49
|
+
var import_react = require("react");
|
|
50
|
+
var activeTraps = [];
|
|
51
|
+
function useFocusTrap(containerRef, {
|
|
52
|
+
active,
|
|
53
|
+
initialFocusRef,
|
|
54
|
+
returnFocusRef,
|
|
55
|
+
restoreFocus = true
|
|
56
|
+
}) {
|
|
57
|
+
const openerRef = (0, import_react.useRef)(null);
|
|
58
|
+
if (active && !openerRef.current && typeof document !== "undefined") {
|
|
59
|
+
openerRef.current = document.activeElement;
|
|
60
|
+
}
|
|
61
|
+
(0, import_react.useEffect)(() => {
|
|
62
|
+
const container = containerRef.current;
|
|
63
|
+
if (!active || !container) return;
|
|
64
|
+
const token = {};
|
|
65
|
+
activeTraps.push(token);
|
|
66
|
+
if (!container.contains(document.activeElement)) {
|
|
67
|
+
const target = initialFocusRef?.current ?? focusableWithin(container)[0] ?? container;
|
|
68
|
+
if (target === container && !container.hasAttribute("tabindex")) {
|
|
69
|
+
container.setAttribute("tabindex", "-1");
|
|
70
|
+
}
|
|
71
|
+
target.focus();
|
|
72
|
+
}
|
|
73
|
+
function onKeyDown(event) {
|
|
74
|
+
if (event.key !== "Tab" || !container) return;
|
|
75
|
+
if (activeTraps[activeTraps.length - 1] !== token) return;
|
|
76
|
+
const items = focusableWithin(container);
|
|
77
|
+
if (items.length === 0) {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const target = nextFocusTarget(
|
|
82
|
+
items,
|
|
83
|
+
document.activeElement,
|
|
84
|
+
event.shiftKey,
|
|
85
|
+
container
|
|
86
|
+
);
|
|
87
|
+
if (target) {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
target.focus();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
document.addEventListener("keydown", onKeyDown);
|
|
93
|
+
return () => {
|
|
94
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
95
|
+
activeTraps.splice(activeTraps.indexOf(token), 1);
|
|
96
|
+
const opener = returnFocusRef?.current ?? openerRef.current;
|
|
97
|
+
openerRef.current = null;
|
|
98
|
+
if (restoreFocus && opener?.isConnected) opener.focus();
|
|
99
|
+
};
|
|
100
|
+
}, [active, containerRef, initialFocusRef, returnFocusRef, restoreFocus]);
|
|
101
|
+
}
|
|
102
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
103
|
+
0 && (module.exports = {
|
|
104
|
+
FOCUSABLE_SELECTOR,
|
|
105
|
+
focusableWithin,
|
|
106
|
+
nextFocusTarget,
|
|
107
|
+
useFocusTrap
|
|
108
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
declare const FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
4
|
+
declare function focusableWithin(container: HTMLElement): HTMLElement[];
|
|
5
|
+
/** The element Tab must be redirected to, or null when the browser's own move is fine. */
|
|
6
|
+
declare function nextFocusTarget(items: HTMLElement[], current: Element | null, shiftKey: boolean, container: HTMLElement): HTMLElement | null;
|
|
7
|
+
|
|
8
|
+
type UseFocusTrapOptions = {
|
|
9
|
+
active: boolean;
|
|
10
|
+
initialFocusRef?: RefObject<HTMLElement | null>;
|
|
11
|
+
/** Wins over the captured opener. Safari leaves <body> focused after a tap on a button. */
|
|
12
|
+
returnFocusRef?: RefObject<HTMLElement | null>;
|
|
13
|
+
restoreFocus?: boolean;
|
|
14
|
+
};
|
|
15
|
+
declare function useFocusTrap(containerRef: RefObject<HTMLElement | null>, { active, initialFocusRef, returnFocusRef, restoreFocus, }: UseFocusTrapOptions): void;
|
|
16
|
+
|
|
17
|
+
export { FOCUSABLE_SELECTOR, type UseFocusTrapOptions, focusableWithin, nextFocusTarget, useFocusTrap };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
declare const FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
4
|
+
declare function focusableWithin(container: HTMLElement): HTMLElement[];
|
|
5
|
+
/** The element Tab must be redirected to, or null when the browser's own move is fine. */
|
|
6
|
+
declare function nextFocusTarget(items: HTMLElement[], current: Element | null, shiftKey: boolean, container: HTMLElement): HTMLElement | null;
|
|
7
|
+
|
|
8
|
+
type UseFocusTrapOptions = {
|
|
9
|
+
active: boolean;
|
|
10
|
+
initialFocusRef?: RefObject<HTMLElement | null>;
|
|
11
|
+
/** Wins over the captured opener. Safari leaves <body> focused after a tap on a button. */
|
|
12
|
+
returnFocusRef?: RefObject<HTMLElement | null>;
|
|
13
|
+
restoreFocus?: boolean;
|
|
14
|
+
};
|
|
15
|
+
declare function useFocusTrap(containerRef: RefObject<HTMLElement | null>, { active, initialFocusRef, returnFocusRef, restoreFocus, }: UseFocusTrapOptions): void;
|
|
16
|
+
|
|
17
|
+
export { FOCUSABLE_SELECTOR, type UseFocusTrapOptions, focusableWithin, nextFocusTarget, useFocusTrap };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/focus-trap/focusables.ts
|
|
4
|
+
var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
5
|
+
function focusableWithin(container) {
|
|
6
|
+
return Array.from(
|
|
7
|
+
container.querySelectorAll(FOCUSABLE_SELECTOR)
|
|
8
|
+
).filter((el) => !el.closest("[hidden], [inert]"));
|
|
9
|
+
}
|
|
10
|
+
function nextFocusTarget(items, current, shiftKey, container) {
|
|
11
|
+
const first = items[0];
|
|
12
|
+
const last = items[items.length - 1];
|
|
13
|
+
if (!first || !last) return null;
|
|
14
|
+
if (!current || !container.contains(current)) return shiftKey ? last : first;
|
|
15
|
+
if (shiftKey && (current === first || current === container)) return last;
|
|
16
|
+
if (!shiftKey && current === last) return first;
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/focus-trap/use-focus-trap.ts
|
|
21
|
+
import { useEffect, useRef } from "react";
|
|
22
|
+
var activeTraps = [];
|
|
23
|
+
function useFocusTrap(containerRef, {
|
|
24
|
+
active,
|
|
25
|
+
initialFocusRef,
|
|
26
|
+
returnFocusRef,
|
|
27
|
+
restoreFocus = true
|
|
28
|
+
}) {
|
|
29
|
+
const openerRef = useRef(null);
|
|
30
|
+
if (active && !openerRef.current && typeof document !== "undefined") {
|
|
31
|
+
openerRef.current = document.activeElement;
|
|
32
|
+
}
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const container = containerRef.current;
|
|
35
|
+
if (!active || !container) return;
|
|
36
|
+
const token = {};
|
|
37
|
+
activeTraps.push(token);
|
|
38
|
+
if (!container.contains(document.activeElement)) {
|
|
39
|
+
const target = initialFocusRef?.current ?? focusableWithin(container)[0] ?? container;
|
|
40
|
+
if (target === container && !container.hasAttribute("tabindex")) {
|
|
41
|
+
container.setAttribute("tabindex", "-1");
|
|
42
|
+
}
|
|
43
|
+
target.focus();
|
|
44
|
+
}
|
|
45
|
+
function onKeyDown(event) {
|
|
46
|
+
if (event.key !== "Tab" || !container) return;
|
|
47
|
+
if (activeTraps[activeTraps.length - 1] !== token) return;
|
|
48
|
+
const items = focusableWithin(container);
|
|
49
|
+
if (items.length === 0) {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const target = nextFocusTarget(
|
|
54
|
+
items,
|
|
55
|
+
document.activeElement,
|
|
56
|
+
event.shiftKey,
|
|
57
|
+
container
|
|
58
|
+
);
|
|
59
|
+
if (target) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
target.focus();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
document.addEventListener("keydown", onKeyDown);
|
|
65
|
+
return () => {
|
|
66
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
67
|
+
activeTraps.splice(activeTraps.indexOf(token), 1);
|
|
68
|
+
const opener = returnFocusRef?.current ?? openerRef.current;
|
|
69
|
+
openerRef.current = null;
|
|
70
|
+
if (restoreFocus && opener?.isConnected) opener.focus();
|
|
71
|
+
};
|
|
72
|
+
}, [active, containerRef, initialFocusRef, returnFocusRef, restoreFocus]);
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
FOCUSABLE_SELECTOR,
|
|
76
|
+
focusableWithin,
|
|
77
|
+
nextFocusTarget,
|
|
78
|
+
useFocusTrap
|
|
79
|
+
};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
BACK_ZONE_PX: () => BACK_ZONE_PX,
|
|
24
|
+
CLOSE_DISTANCE_PX: () => CLOSE_DISTANCE_PX,
|
|
25
|
+
FOCUSABLE_SELECTOR: () => FOCUSABLE_SELECTOR,
|
|
26
|
+
FORWARD_ZONE_PX: () => FORWARD_ZONE_PX,
|
|
27
|
+
MAX_SWIPE_DURATION_MS: () => MAX_SWIPE_DURATION_MS,
|
|
28
|
+
OPEN_DISTANCE_PX: () => OPEN_DISTANCE_PX,
|
|
29
|
+
VERTICAL_CANCEL_PX: () => VERTICAL_CANCEL_PX,
|
|
30
|
+
copyText: () => copyText,
|
|
31
|
+
focusableWithin: () => focusableWithin,
|
|
32
|
+
isInOpenZone: () => isInOpenZone,
|
|
33
|
+
isInsideHorizontalScroller: () => isInsideHorizontalScroller,
|
|
34
|
+
nextFocusTarget: () => nextFocusTarget,
|
|
35
|
+
resolveSwipeIntent: () => resolveSwipeIntent,
|
|
36
|
+
useCopyFeedback: () => useCopyFeedback,
|
|
37
|
+
useDrawerSwipe: () => useDrawerSwipe,
|
|
38
|
+
useEscape: () => useEscape,
|
|
39
|
+
useFocusTrap: () => useFocusTrap
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(src_exports);
|
|
42
|
+
|
|
43
|
+
// src/drawer-swipe/swipe-intent.ts
|
|
44
|
+
var BACK_ZONE_PX = 60;
|
|
45
|
+
var FORWARD_ZONE_PX = 30;
|
|
46
|
+
var OPEN_DISTANCE_PX = 80;
|
|
47
|
+
var CLOSE_DISTANCE_PX = 60;
|
|
48
|
+
var VERTICAL_CANCEL_PX = 40;
|
|
49
|
+
var MAX_SWIPE_DURATION_MS = 450;
|
|
50
|
+
var HORIZONTAL_DOMINANCE = 1.5;
|
|
51
|
+
function isInOpenZone(x, viewportWidth) {
|
|
52
|
+
return x > BACK_ZONE_PX && x < viewportWidth - FORWARD_ZONE_PX;
|
|
53
|
+
}
|
|
54
|
+
function resolveSwipeIntent(start, end, drawerOpen, viewportWidth) {
|
|
55
|
+
const dx = end.x - start.x;
|
|
56
|
+
const dy = end.y - start.y;
|
|
57
|
+
if (end.time - start.time > MAX_SWIPE_DURATION_MS) return null;
|
|
58
|
+
if (Math.abs(dx) < Math.abs(dy) * HORIZONTAL_DOMINANCE) return null;
|
|
59
|
+
if (drawerOpen) {
|
|
60
|
+
return dx <= -CLOSE_DISTANCE_PX ? "close" : null;
|
|
61
|
+
}
|
|
62
|
+
if (dx < OPEN_DISTANCE_PX) return null;
|
|
63
|
+
return isInOpenZone(start.x, viewportWidth) ? "open" : null;
|
|
64
|
+
}
|
|
65
|
+
function isInsideHorizontalScroller(target) {
|
|
66
|
+
let node = target instanceof Element ? target : null;
|
|
67
|
+
while (node && node !== document.body) {
|
|
68
|
+
const { overflowX } = window.getComputedStyle(node);
|
|
69
|
+
if ((overflowX === "auto" || overflowX === "scroll") && node.scrollWidth > node.clientWidth) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
node = node.parentElement;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/drawer-swipe/use-drawer-swipe.ts
|
|
78
|
+
var import_react = require("react");
|
|
79
|
+
function isWideViewport(maxViewportWidth) {
|
|
80
|
+
return window.matchMedia(`(min-width: ${maxViewportWidth}px)`).matches;
|
|
81
|
+
}
|
|
82
|
+
function useDrawerSwipe({
|
|
83
|
+
drawerOpen,
|
|
84
|
+
onOpen,
|
|
85
|
+
onClose,
|
|
86
|
+
enabled = true,
|
|
87
|
+
maxViewportWidth = 768
|
|
88
|
+
}) {
|
|
89
|
+
const start = (0, import_react.useRef)(null);
|
|
90
|
+
const handlers = (0, import_react.useRef)({ drawerOpen, onOpen, onClose, maxViewportWidth });
|
|
91
|
+
handlers.current = { drawerOpen, onOpen, onClose, maxViewportWidth };
|
|
92
|
+
(0, import_react.useEffect)(() => {
|
|
93
|
+
if (!enabled) return;
|
|
94
|
+
function handleStart(event) {
|
|
95
|
+
start.current = null;
|
|
96
|
+
if (event.touches.length !== 1) return;
|
|
97
|
+
if (isWideViewport(handlers.current.maxViewportWidth)) return;
|
|
98
|
+
if (!handlers.current.drawerOpen && isInsideHorizontalScroller(event.target)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const touch = event.touches[0];
|
|
102
|
+
if (!touch) return;
|
|
103
|
+
start.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
|
|
104
|
+
}
|
|
105
|
+
function handleMove(event) {
|
|
106
|
+
const origin = start.current;
|
|
107
|
+
const touch = event.touches[0];
|
|
108
|
+
if (!origin || !touch) return;
|
|
109
|
+
if (Math.abs(touch.clientY - origin.y) > VERTICAL_CANCEL_PX) {
|
|
110
|
+
start.current = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function handleEnd(event) {
|
|
114
|
+
const origin = start.current;
|
|
115
|
+
start.current = null;
|
|
116
|
+
const touch = event.changedTouches[0];
|
|
117
|
+
if (!origin || !touch) return;
|
|
118
|
+
const { drawerOpen: drawerOpen2, onOpen: onOpen2, onClose: onClose2 } = handlers.current;
|
|
119
|
+
const intent = resolveSwipeIntent(
|
|
120
|
+
origin,
|
|
121
|
+
{ x: touch.clientX, y: touch.clientY, time: Date.now() },
|
|
122
|
+
drawerOpen2,
|
|
123
|
+
window.innerWidth
|
|
124
|
+
);
|
|
125
|
+
if (intent === "open") onOpen2();
|
|
126
|
+
if (intent === "close") onClose2();
|
|
127
|
+
}
|
|
128
|
+
function handleCancel() {
|
|
129
|
+
start.current = null;
|
|
130
|
+
}
|
|
131
|
+
document.addEventListener("touchstart", handleStart, { passive: true });
|
|
132
|
+
document.addEventListener("touchmove", handleMove, { passive: true });
|
|
133
|
+
document.addEventListener("touchend", handleEnd, { passive: true });
|
|
134
|
+
document.addEventListener("touchcancel", handleCancel, { passive: true });
|
|
135
|
+
return () => {
|
|
136
|
+
document.removeEventListener("touchstart", handleStart);
|
|
137
|
+
document.removeEventListener("touchmove", handleMove);
|
|
138
|
+
document.removeEventListener("touchend", handleEnd);
|
|
139
|
+
document.removeEventListener("touchcancel", handleCancel);
|
|
140
|
+
};
|
|
141
|
+
}, [enabled]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/focus-trap/focusables.ts
|
|
145
|
+
var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
146
|
+
function focusableWithin(container) {
|
|
147
|
+
return Array.from(
|
|
148
|
+
container.querySelectorAll(FOCUSABLE_SELECTOR)
|
|
149
|
+
).filter((el) => !el.closest("[hidden], [inert]"));
|
|
150
|
+
}
|
|
151
|
+
function nextFocusTarget(items, current, shiftKey, container) {
|
|
152
|
+
const first = items[0];
|
|
153
|
+
const last = items[items.length - 1];
|
|
154
|
+
if (!first || !last) return null;
|
|
155
|
+
if (!current || !container.contains(current)) return shiftKey ? last : first;
|
|
156
|
+
if (shiftKey && (current === first || current === container)) return last;
|
|
157
|
+
if (!shiftKey && current === last) return first;
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/focus-trap/use-focus-trap.ts
|
|
162
|
+
var import_react2 = require("react");
|
|
163
|
+
var activeTraps = [];
|
|
164
|
+
function useFocusTrap(containerRef, {
|
|
165
|
+
active,
|
|
166
|
+
initialFocusRef,
|
|
167
|
+
returnFocusRef,
|
|
168
|
+
restoreFocus = true
|
|
169
|
+
}) {
|
|
170
|
+
const openerRef = (0, import_react2.useRef)(null);
|
|
171
|
+
if (active && !openerRef.current && typeof document !== "undefined") {
|
|
172
|
+
openerRef.current = document.activeElement;
|
|
173
|
+
}
|
|
174
|
+
(0, import_react2.useEffect)(() => {
|
|
175
|
+
const container = containerRef.current;
|
|
176
|
+
if (!active || !container) return;
|
|
177
|
+
const token = {};
|
|
178
|
+
activeTraps.push(token);
|
|
179
|
+
if (!container.contains(document.activeElement)) {
|
|
180
|
+
const target = initialFocusRef?.current ?? focusableWithin(container)[0] ?? container;
|
|
181
|
+
if (target === container && !container.hasAttribute("tabindex")) {
|
|
182
|
+
container.setAttribute("tabindex", "-1");
|
|
183
|
+
}
|
|
184
|
+
target.focus();
|
|
185
|
+
}
|
|
186
|
+
function onKeyDown(event) {
|
|
187
|
+
if (event.key !== "Tab" || !container) return;
|
|
188
|
+
if (activeTraps[activeTraps.length - 1] !== token) return;
|
|
189
|
+
const items = focusableWithin(container);
|
|
190
|
+
if (items.length === 0) {
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const target = nextFocusTarget(
|
|
195
|
+
items,
|
|
196
|
+
document.activeElement,
|
|
197
|
+
event.shiftKey,
|
|
198
|
+
container
|
|
199
|
+
);
|
|
200
|
+
if (target) {
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
target.focus();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
document.addEventListener("keydown", onKeyDown);
|
|
206
|
+
return () => {
|
|
207
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
208
|
+
activeTraps.splice(activeTraps.indexOf(token), 1);
|
|
209
|
+
const opener = returnFocusRef?.current ?? openerRef.current;
|
|
210
|
+
openerRef.current = null;
|
|
211
|
+
if (restoreFocus && opener?.isConnected) opener.focus();
|
|
212
|
+
};
|
|
213
|
+
}, [active, containerRef, initialFocusRef, returnFocusRef, restoreFocus]);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/escape/use-escape.ts
|
|
217
|
+
var import_react3 = require("react");
|
|
218
|
+
function useEscape(onEscape, active = true) {
|
|
219
|
+
const handlerRef = (0, import_react3.useRef)(onEscape);
|
|
220
|
+
(0, import_react3.useEffect)(() => {
|
|
221
|
+
handlerRef.current = onEscape;
|
|
222
|
+
});
|
|
223
|
+
(0, import_react3.useEffect)(() => {
|
|
224
|
+
if (!active) return;
|
|
225
|
+
function onKeyDown(event) {
|
|
226
|
+
if (event.key === "Escape") handlerRef.current();
|
|
227
|
+
}
|
|
228
|
+
document.addEventListener("keydown", onKeyDown);
|
|
229
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
230
|
+
}, [active]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/clipboard/copy-text.ts
|
|
234
|
+
async function copyText(text) {
|
|
235
|
+
const hasClipboardApi = typeof navigator !== "undefined" && !!navigator.clipboard?.writeText && typeof window !== "undefined" && window.isSecureContext;
|
|
236
|
+
if (hasClipboardApi) {
|
|
237
|
+
try {
|
|
238
|
+
await navigator.clipboard.writeText(text);
|
|
239
|
+
return { ok: true };
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (execCommandCopy(text)) return { ok: true };
|
|
244
|
+
return { ok: false, reason: hasClipboardApi ? "denied" : "unavailable" };
|
|
245
|
+
}
|
|
246
|
+
function execCommandCopy(text) {
|
|
247
|
+
if (typeof document === "undefined") return false;
|
|
248
|
+
const textarea = document.createElement("textarea");
|
|
249
|
+
textarea.value = text;
|
|
250
|
+
textarea.readOnly = true;
|
|
251
|
+
textarea.style.position = "fixed";
|
|
252
|
+
textarea.style.top = "-1000px";
|
|
253
|
+
textarea.style.left = "-1000px";
|
|
254
|
+
textarea.style.fontSize = "16px";
|
|
255
|
+
document.body.appendChild(textarea);
|
|
256
|
+
try {
|
|
257
|
+
textarea.select();
|
|
258
|
+
textarea.setSelectionRange(0, text.length);
|
|
259
|
+
return document.execCommand("copy");
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
} finally {
|
|
263
|
+
document.body.removeChild(textarea);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/clipboard/use-copy-feedback.ts
|
|
268
|
+
var import_react4 = require("react");
|
|
269
|
+
function useCopyFeedback({ resetMs = 1500 } = {}) {
|
|
270
|
+
const [status, setStatus] = (0, import_react4.useState)("idle");
|
|
271
|
+
const timerRef = (0, import_react4.useRef)(void 0);
|
|
272
|
+
const clearTimer = (0, import_react4.useCallback)(() => {
|
|
273
|
+
if (timerRef.current !== void 0) clearTimeout(timerRef.current);
|
|
274
|
+
timerRef.current = void 0;
|
|
275
|
+
}, []);
|
|
276
|
+
(0, import_react4.useEffect)(() => clearTimer, [clearTimer]);
|
|
277
|
+
const copy = (0, import_react4.useCallback)(
|
|
278
|
+
async (text) => {
|
|
279
|
+
const result = await copyText(text);
|
|
280
|
+
clearTimer();
|
|
281
|
+
setStatus(result.ok ? "copied" : "failed");
|
|
282
|
+
timerRef.current = setTimeout(() => setStatus("idle"), resetMs);
|
|
283
|
+
return result;
|
|
284
|
+
},
|
|
285
|
+
[clearTimer, resetMs]
|
|
286
|
+
);
|
|
287
|
+
return { copy, status };
|
|
288
|
+
}
|
|
289
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
290
|
+
0 && (module.exports = {
|
|
291
|
+
BACK_ZONE_PX,
|
|
292
|
+
CLOSE_DISTANCE_PX,
|
|
293
|
+
FOCUSABLE_SELECTOR,
|
|
294
|
+
FORWARD_ZONE_PX,
|
|
295
|
+
MAX_SWIPE_DURATION_MS,
|
|
296
|
+
OPEN_DISTANCE_PX,
|
|
297
|
+
VERTICAL_CANCEL_PX,
|
|
298
|
+
copyText,
|
|
299
|
+
focusableWithin,
|
|
300
|
+
isInOpenZone,
|
|
301
|
+
isInsideHorizontalScroller,
|
|
302
|
+
nextFocusTarget,
|
|
303
|
+
resolveSwipeIntent,
|
|
304
|
+
useCopyFeedback,
|
|
305
|
+
useDrawerSwipe,
|
|
306
|
+
useEscape,
|
|
307
|
+
useFocusTrap
|
|
308
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
type SwipePoint = {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
time: number;
|
|
7
|
+
};
|
|
8
|
+
declare const BACK_ZONE_PX = 60;
|
|
9
|
+
declare const FORWARD_ZONE_PX = 30;
|
|
10
|
+
declare const OPEN_DISTANCE_PX = 80;
|
|
11
|
+
declare const CLOSE_DISTANCE_PX = 60;
|
|
12
|
+
declare const VERTICAL_CANCEL_PX = 40;
|
|
13
|
+
declare const MAX_SWIPE_DURATION_MS = 450;
|
|
14
|
+
type SwipeIntent = 'open' | 'close' | null;
|
|
15
|
+
declare function isInOpenZone(x: number, viewportWidth: number): boolean;
|
|
16
|
+
declare function resolveSwipeIntent(start: SwipePoint, end: SwipePoint, drawerOpen: boolean, viewportWidth: number): SwipeIntent;
|
|
17
|
+
declare function isInsideHorizontalScroller(target: EventTarget | null): boolean;
|
|
18
|
+
|
|
19
|
+
type UseDrawerSwipeOptions = {
|
|
20
|
+
drawerOpen: boolean;
|
|
21
|
+
onOpen: () => void;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
maxViewportWidth?: number;
|
|
25
|
+
};
|
|
26
|
+
declare function useDrawerSwipe({ drawerOpen, onOpen, onClose, enabled, maxViewportWidth, }: UseDrawerSwipeOptions): void;
|
|
27
|
+
|
|
28
|
+
declare const FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
29
|
+
declare function focusableWithin(container: HTMLElement): HTMLElement[];
|
|
30
|
+
/** The element Tab must be redirected to, or null when the browser's own move is fine. */
|
|
31
|
+
declare function nextFocusTarget(items: HTMLElement[], current: Element | null, shiftKey: boolean, container: HTMLElement): HTMLElement | null;
|
|
32
|
+
|
|
33
|
+
type UseFocusTrapOptions = {
|
|
34
|
+
active: boolean;
|
|
35
|
+
initialFocusRef?: RefObject<HTMLElement | null>;
|
|
36
|
+
/** Wins over the captured opener. Safari leaves <body> focused after a tap on a button. */
|
|
37
|
+
returnFocusRef?: RefObject<HTMLElement | null>;
|
|
38
|
+
restoreFocus?: boolean;
|
|
39
|
+
};
|
|
40
|
+
declare function useFocusTrap(containerRef: RefObject<HTMLElement | null>, { active, initialFocusRef, returnFocusRef, restoreFocus, }: UseFocusTrapOptions): void;
|
|
41
|
+
|
|
42
|
+
declare function useEscape(onEscape: () => void, active?: boolean): void;
|
|
43
|
+
|
|
44
|
+
type CopyResult = {
|
|
45
|
+
ok: true;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
reason: 'unavailable' | 'denied';
|
|
49
|
+
};
|
|
50
|
+
declare function copyText(text: string): Promise<CopyResult>;
|
|
51
|
+
|
|
52
|
+
type CopyStatus = 'idle' | 'copied' | 'failed';
|
|
53
|
+
declare function useCopyFeedback({ resetMs }?: {
|
|
54
|
+
resetMs?: number;
|
|
55
|
+
}): {
|
|
56
|
+
copy: (text: string) => Promise<CopyResult>;
|
|
57
|
+
status: CopyStatus;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export { BACK_ZONE_PX, CLOSE_DISTANCE_PX, type CopyResult, type CopyStatus, FOCUSABLE_SELECTOR, FORWARD_ZONE_PX, MAX_SWIPE_DURATION_MS, OPEN_DISTANCE_PX, type SwipeIntent, type SwipePoint, type UseDrawerSwipeOptions, type UseFocusTrapOptions, VERTICAL_CANCEL_PX, copyText, focusableWithin, isInOpenZone, isInsideHorizontalScroller, nextFocusTarget, resolveSwipeIntent, useCopyFeedback, useDrawerSwipe, useEscape, useFocusTrap };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
type SwipePoint = {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
time: number;
|
|
7
|
+
};
|
|
8
|
+
declare const BACK_ZONE_PX = 60;
|
|
9
|
+
declare const FORWARD_ZONE_PX = 30;
|
|
10
|
+
declare const OPEN_DISTANCE_PX = 80;
|
|
11
|
+
declare const CLOSE_DISTANCE_PX = 60;
|
|
12
|
+
declare const VERTICAL_CANCEL_PX = 40;
|
|
13
|
+
declare const MAX_SWIPE_DURATION_MS = 450;
|
|
14
|
+
type SwipeIntent = 'open' | 'close' | null;
|
|
15
|
+
declare function isInOpenZone(x: number, viewportWidth: number): boolean;
|
|
16
|
+
declare function resolveSwipeIntent(start: SwipePoint, end: SwipePoint, drawerOpen: boolean, viewportWidth: number): SwipeIntent;
|
|
17
|
+
declare function isInsideHorizontalScroller(target: EventTarget | null): boolean;
|
|
18
|
+
|
|
19
|
+
type UseDrawerSwipeOptions = {
|
|
20
|
+
drawerOpen: boolean;
|
|
21
|
+
onOpen: () => void;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
maxViewportWidth?: number;
|
|
25
|
+
};
|
|
26
|
+
declare function useDrawerSwipe({ drawerOpen, onOpen, onClose, enabled, maxViewportWidth, }: UseDrawerSwipeOptions): void;
|
|
27
|
+
|
|
28
|
+
declare const FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
29
|
+
declare function focusableWithin(container: HTMLElement): HTMLElement[];
|
|
30
|
+
/** The element Tab must be redirected to, or null when the browser's own move is fine. */
|
|
31
|
+
declare function nextFocusTarget(items: HTMLElement[], current: Element | null, shiftKey: boolean, container: HTMLElement): HTMLElement | null;
|
|
32
|
+
|
|
33
|
+
type UseFocusTrapOptions = {
|
|
34
|
+
active: boolean;
|
|
35
|
+
initialFocusRef?: RefObject<HTMLElement | null>;
|
|
36
|
+
/** Wins over the captured opener. Safari leaves <body> focused after a tap on a button. */
|
|
37
|
+
returnFocusRef?: RefObject<HTMLElement | null>;
|
|
38
|
+
restoreFocus?: boolean;
|
|
39
|
+
};
|
|
40
|
+
declare function useFocusTrap(containerRef: RefObject<HTMLElement | null>, { active, initialFocusRef, returnFocusRef, restoreFocus, }: UseFocusTrapOptions): void;
|
|
41
|
+
|
|
42
|
+
declare function useEscape(onEscape: () => void, active?: boolean): void;
|
|
43
|
+
|
|
44
|
+
type CopyResult = {
|
|
45
|
+
ok: true;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
reason: 'unavailable' | 'denied';
|
|
49
|
+
};
|
|
50
|
+
declare function copyText(text: string): Promise<CopyResult>;
|
|
51
|
+
|
|
52
|
+
type CopyStatus = 'idle' | 'copied' | 'failed';
|
|
53
|
+
declare function useCopyFeedback({ resetMs }?: {
|
|
54
|
+
resetMs?: number;
|
|
55
|
+
}): {
|
|
56
|
+
copy: (text: string) => Promise<CopyResult>;
|
|
57
|
+
status: CopyStatus;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export { BACK_ZONE_PX, CLOSE_DISTANCE_PX, type CopyResult, type CopyStatus, FOCUSABLE_SELECTOR, FORWARD_ZONE_PX, MAX_SWIPE_DURATION_MS, OPEN_DISTANCE_PX, type SwipeIntent, type SwipePoint, type UseDrawerSwipeOptions, type UseFocusTrapOptions, VERTICAL_CANCEL_PX, copyText, focusableWithin, isInOpenZone, isInsideHorizontalScroller, nextFocusTarget, resolveSwipeIntent, useCopyFeedback, useDrawerSwipe, useEscape, useFocusTrap };
|