@t007/utils 0.0.24 → 0.0.26
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 +91 -4
- package/dist/arrowNavigation-DK8mqVOk.d.cts +31 -0
- package/dist/arrowNavigation-VenvPI4H.d.ts +31 -0
- package/dist/chunk-AI5O3OGE.js +165 -0
- package/dist/chunk-OGFBI6PS.js +16 -0
- package/dist/chunk-XVFFZZJA.js +185 -0
- package/dist/components/react.cjs +51 -0
- package/dist/components/react.d.cts +17 -0
- package/dist/components/react.d.ts +17 -0
- package/dist/components/react.js +15 -0
- package/dist/hooks/react.cjs +466 -0
- package/dist/hooks/react.d.cts +46 -0
- package/dist/hooks/react.d.ts +46 -0
- package/dist/hooks/react.js +276 -0
- package/dist/hooks/vanilla.cjs +407 -0
- package/dist/hooks/vanilla.d.cts +35 -0
- package/dist/hooks/vanilla.d.ts +35 -0
- package/dist/hooks/vanilla.js +232 -0
- package/dist/index.cjs +29 -96
- package/dist/index.d.cts +25 -65
- package/dist/index.d.ts +25 -65
- package/dist/index.js +55 -215
- package/dist/ripple-DcMWw_AP.d.cts +71 -0
- package/dist/ripple-DcMWw_AP.d.ts +71 -0
- package/dist/scrollAssist-y9wFmYgt.d.cts +72 -0
- package/dist/scrollAssist-y9wFmYgt.d.ts +72 -0
- package/dist/styles/ripple.css +55 -0
- package/dist/styles/scroll-assist.css +44 -0
- package/dist/useHighlight-DMpDCILK.d.cts +19 -0
- package/dist/useHighlight-DMpDCILK.d.ts +19 -0
- package/package.json +42 -4
|
@@ -0,0 +1,466 @@
|
|
|
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/hooks/react.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
useArrowNavigation: () => useArrowNavigation,
|
|
24
|
+
useFocusTrap: () => useFocusTrap,
|
|
25
|
+
useHighlight: () => useHighlight,
|
|
26
|
+
useOutsideClick: () => useOutsideClick,
|
|
27
|
+
useRipple: () => useRipple,
|
|
28
|
+
useScrollAssist: () => useScrollAssist
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(react_exports);
|
|
31
|
+
|
|
32
|
+
// src/hooks/react/useScrollAssist.ts
|
|
33
|
+
var import_react = require("react");
|
|
34
|
+
|
|
35
|
+
// src/core/num.ts
|
|
36
|
+
var import_utils = require("sia-reactor/utils");
|
|
37
|
+
|
|
38
|
+
// src/core/dom.ts
|
|
39
|
+
var import_utils2 = require("sia-reactor/utils");
|
|
40
|
+
var INTERACTIVE_SELECTOR = 'button,[href],input,label,select,textarea,details>summary,[contenteditable],iframe,audio[controls],video[controls],[tabindex]:not([tabindex="-1"])';
|
|
41
|
+
var isInteractive = (target) => target instanceof HTMLElement && target.matches(INTERACTIVE_SELECTOR);
|
|
42
|
+
function getActiveElement(root = document) {
|
|
43
|
+
const activeEl = root.activeElement;
|
|
44
|
+
return !activeEl ? null : activeEl.shadowRoot ? getActiveElement(activeEl.shadowRoot) : activeEl;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/hooks/react/useScrollAssist.ts
|
|
48
|
+
var import_sia_reactor = require("sia-reactor");
|
|
49
|
+
function useScrollAssist(ref, { enabled = true, pxPerSecond = 80, assistClassName = "t007-scroll-assist", vertical = true, horizontal = true } = import_sia_reactor.NIL) {
|
|
50
|
+
const scrollId = (0, import_react.useRef)(null), last = (0, import_react.useRef)(performance.now()), assists = (0, import_react.useRef)({}), assistWidth = (0, import_react.useRef)(20), assistHeight = (0, import_react.useRef)(20);
|
|
51
|
+
const update = (0, import_react.useCallback)(() => {
|
|
52
|
+
const el = ref.current;
|
|
53
|
+
if (!el) return;
|
|
54
|
+
const hasInteractive = !!el.querySelector(INTERACTIVE_SELECTOR);
|
|
55
|
+
if (horizontal) {
|
|
56
|
+
const w = assists.current.left?.offsetWidth || assistWidth.current;
|
|
57
|
+
const check = hasInteractive ? el.clientWidth < w * 2 : false;
|
|
58
|
+
assists.current.left.style.display = check ? "none" : el.scrollLeft > 0 ? "block" : "none";
|
|
59
|
+
assists.current.right.style.display = check ? "none" : el.scrollLeft + el.clientWidth < el.scrollWidth - 1 ? "block" : "none";
|
|
60
|
+
assistWidth.current = w;
|
|
61
|
+
}
|
|
62
|
+
if (vertical) {
|
|
63
|
+
const h = assists.current.up?.offsetHeight || assistHeight.current;
|
|
64
|
+
const check = hasInteractive ? el.clientHeight < h * 2 : false;
|
|
65
|
+
assists.current.up.style.display = check ? "none" : el.scrollTop > 0 ? "block" : "none";
|
|
66
|
+
assists.current.down.style.display = check ? "none" : el.scrollTop + el.clientHeight < el.scrollHeight - 1 ? "block" : "none";
|
|
67
|
+
assistHeight.current = h;
|
|
68
|
+
}
|
|
69
|
+
}, [ref, horizontal, vertical]);
|
|
70
|
+
const scroll = (0, import_react.useCallback)(
|
|
71
|
+
(dir) => {
|
|
72
|
+
const el = ref.current;
|
|
73
|
+
if (!el) return;
|
|
74
|
+
const loop = () => {
|
|
75
|
+
const now = performance.now(), dt = now - last.current;
|
|
76
|
+
last.current = now;
|
|
77
|
+
const d = pxPerSecond * dt / 1e3;
|
|
78
|
+
if (dir === "left") el.scrollLeft = Math.max(0, el.scrollLeft - d);
|
|
79
|
+
if (dir === "right") el.scrollLeft = Math.min(el.scrollWidth - el.clientWidth, el.scrollLeft + d);
|
|
80
|
+
if (dir === "up") el.scrollTop = Math.max(0, el.scrollTop - d);
|
|
81
|
+
if (dir === "down") el.scrollTop = Math.min(el.scrollHeight - el.clientHeight, el.scrollTop + d);
|
|
82
|
+
scrollId.current = requestAnimationFrame(loop);
|
|
83
|
+
};
|
|
84
|
+
last.current = performance.now();
|
|
85
|
+
loop();
|
|
86
|
+
},
|
|
87
|
+
[ref, pxPerSecond]
|
|
88
|
+
);
|
|
89
|
+
const stop = (0, import_react.useCallback)(() => void (cancelAnimationFrame(scrollId.current ?? 0), scrollId.current = null), []);
|
|
90
|
+
const createAssist = (0, import_react.useCallback)(
|
|
91
|
+
(dir) => {
|
|
92
|
+
const el = document.createElement("div");
|
|
93
|
+
el.className = assistClassName;
|
|
94
|
+
el.dataset.scrollDirection = dir;
|
|
95
|
+
el.style.display = "none";
|
|
96
|
+
for (const evt of ["pointerenter", "dragenter"]) el.addEventListener(evt, () => scroll(dir));
|
|
97
|
+
for (const evt of ["pointerleave", "pointerup", "pointercancel", "dragleave", "dragend"]) el.addEventListener(evt, stop);
|
|
98
|
+
return el;
|
|
99
|
+
},
|
|
100
|
+
[assistClassName, scroll, stop]
|
|
101
|
+
);
|
|
102
|
+
(0, import_react.useEffect)(() => {
|
|
103
|
+
if (!enabled) return;
|
|
104
|
+
const el = ref.current;
|
|
105
|
+
if (!el || !el.parentElement) return;
|
|
106
|
+
const parent = el.parentElement;
|
|
107
|
+
const dirs = [...horizontal ? ["left", "right"] : [], ...vertical ? ["up", "down"] : []];
|
|
108
|
+
for (const dir of dirs) {
|
|
109
|
+
const div = createAssist(dir);
|
|
110
|
+
assists.current[dir] = div;
|
|
111
|
+
(dir === "left" || dir === "up" ? parent.insertBefore : parent.appendChild).call(parent, div, el);
|
|
112
|
+
}
|
|
113
|
+
const observer = new ResizeObserver(update), mObserver = new MutationObserver(update);
|
|
114
|
+
update(), el.addEventListener("scroll", update);
|
|
115
|
+
observer.observe(el), mObserver.observe(el, { childList: true, subtree: true, characterData: true });
|
|
116
|
+
const allAssists = assists.current;
|
|
117
|
+
return () => {
|
|
118
|
+
stop(), el.removeEventListener("scroll", update);
|
|
119
|
+
observer.disconnect(), mObserver.disconnect();
|
|
120
|
+
for (const a of Object.values(allAssists)) a?.remove();
|
|
121
|
+
};
|
|
122
|
+
}, [ref, enabled, assistClassName, vertical, horizontal, createAssist, update, stop]);
|
|
123
|
+
return { update };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/hooks/react/useOutsideClick.ts
|
|
127
|
+
var import_react2 = require("react");
|
|
128
|
+
|
|
129
|
+
// src/hooks/vanilla/outsideClick.ts
|
|
130
|
+
var import_sia_reactor2 = require("sia-reactor");
|
|
131
|
+
var stacks = /* @__PURE__ */ new WeakMap();
|
|
132
|
+
function initOutsideClick(el, { enabled = false, onOutsideClick = import_sia_reactor2.NOOP, clickOnClick = true, clickOnEscape = true, clickOnFocusOut = false, allowInputs = true, root = window, scoped = true, capture = true } = import_sia_reactor2.NIL) {
|
|
133
|
+
const existing = (t007._outsiders ??= /* @__PURE__ */ new WeakMap()).get(el);
|
|
134
|
+
if (!enabled || existing) return existing ? existing : void 0;
|
|
135
|
+
scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
|
|
136
|
+
const stack = stacks.get(root) ?? [], onScopedOut = (e, t, p = e.touches?.[0] || e, rect = el.getBoundingClientRect()) => {
|
|
137
|
+
if (stack.at(-1) !== el || p.clientX >= rect.left && p.clientX <= rect.right && p.clientY >= rect.top && p.clientY <= rect.bottom) return false;
|
|
138
|
+
return (!scoped ? true : root.contains(t)) && onOutsideClick(e);
|
|
139
|
+
}, handleClick = ((e) => clickOnClick && !(allowInputs && isInteractive(e.target)) && onScopedOut(e, e.target)), handleEscape = ((e) => clickOnEscape && e.key === "Escape" && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && stack.at(-1) === el && onOutsideClick(e)), handleFocusOut = (e) => clickOnFocusOut && onScopedOut(e, e.relatedTarget);
|
|
140
|
+
root.addEventListener("mousedown", handleClick, capture), root.addEventListener("touchstart", handleClick, { passive: true, capture });
|
|
141
|
+
root.addEventListener("keydown", handleEscape, capture), el.addEventListener("focusout", handleFocusOut, capture);
|
|
142
|
+
if (!stack.includes(el)) stack.push(el), stacks.set(root, stack);
|
|
143
|
+
const destroy = () => {
|
|
144
|
+
root.removeEventListener("mousedown", handleClick, capture), root.removeEventListener("touchstart", handleClick, capture);
|
|
145
|
+
root.removeEventListener("keydown", handleEscape, capture);
|
|
146
|
+
el.removeEventListener("focusout", handleFocusOut, capture);
|
|
147
|
+
t007._outsiders.delete(el), stack.splice(stack.indexOf(el), 1);
|
|
148
|
+
};
|
|
149
|
+
return t007._outsiders.set(el, destroy), destroy;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/hooks/react/useOutsideClick.ts
|
|
153
|
+
var import_sia_reactor3 = require("sia-reactor");
|
|
154
|
+
function useOutsideClick(ref, config = import_sia_reactor3.NIL) {
|
|
155
|
+
(0, import_react2.useEffect)(() => ref.current ? initOutsideClick(ref.current, config) : void 0, [ref, config.enabled, config.onOutsideClick, config.clickOnEscape, config.clickOnClick, config.clickOnFocusOut, config.allowInputs, config.root, config.scoped, config.capture]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/hooks/react/useArrowNavigation/index.ts
|
|
159
|
+
var import_react3 = require("react");
|
|
160
|
+
|
|
161
|
+
// src/hooks/react/useArrowNavigation/utils.ts
|
|
162
|
+
var getTargetIndex = ({ key, currIndex, length, gridX, gridY, vGridY, loop, ctrlKey = false, rtl }) => {
|
|
163
|
+
const rowStart = currIndex - currIndex % gridX, rowEnd = Math.min(rowStart + gridX - 1, length - 1), colStart = currIndex % gridX, colEnd = Math.min(colStart + gridX * (gridY - 1), length - 1), canX = gridX > 1, canY = gridY > 1, horizontalMove = rtl ? { ArrowRight: canX ? -1 : 0, ArrowLeft: canX ? 1 : 0 } : { ArrowRight: canX ? 1 : 0, ArrowLeft: canX ? -1 : 0 }, move = { ...horizontalMove, ArrowDown: canY ? gridX : 0, ArrowUp: canY ? -gridX : 0, Home: ctrlKey ? 0 : rowStart, End: ctrlKey ? length - 1 : rowEnd, PageDown: (vGridY - 1) * gridX, PageUp: -(vGridY - 1) * gridX }[key] ?? 0;
|
|
164
|
+
let targetIndex = key === "Home" || key === "End" ? move : currIndex + move;
|
|
165
|
+
if (key === "ArrowDown") {
|
|
166
|
+
if (!loop && targetIndex >= length) targetIndex -= move;
|
|
167
|
+
} else if (key === "ArrowUp") {
|
|
168
|
+
if (!loop && targetIndex < 0) targetIndex += Math.abs(move);
|
|
169
|
+
} else if (key === "PageDown") {
|
|
170
|
+
if (!loop && targetIndex >= length) targetIndex = colEnd;
|
|
171
|
+
} else if (key === "PageUp") {
|
|
172
|
+
if (!loop && targetIndex < 0) targetIndex = colStart;
|
|
173
|
+
}
|
|
174
|
+
return loop ? (targetIndex + length) % length : (0, import_utils.clamp)(0, targetIndex, length - 1);
|
|
175
|
+
};
|
|
176
|
+
var getCommonAncestor = (first, second) => {
|
|
177
|
+
if (!first) return null;
|
|
178
|
+
if (!second) return first.parentElement;
|
|
179
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
180
|
+
let current = first;
|
|
181
|
+
while (current) ancestors.add(current), current = current.parentElement;
|
|
182
|
+
current = second;
|
|
183
|
+
while (current) {
|
|
184
|
+
if (ancestors.has(current)) return current;
|
|
185
|
+
current = current.parentElement;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
};
|
|
189
|
+
var getGrid = (all, x = true, y = true, vY = true) => {
|
|
190
|
+
const len = all.length, grid = {};
|
|
191
|
+
if (!len) return grid;
|
|
192
|
+
let cols = all.findIndex((el) => el.offsetTop !== all[0].offsetTop);
|
|
193
|
+
cols = cols > 0 ? cols : len;
|
|
194
|
+
if (x) grid.x = cols;
|
|
195
|
+
let rows = Math.ceil(len / cols);
|
|
196
|
+
if (y) grid.y = rows;
|
|
197
|
+
if (vY) {
|
|
198
|
+
const itemHeight = all[0].offsetHeight ?? 0, containerHeight = getCommonAncestor(all[0], all[1])?.clientHeight ?? 0;
|
|
199
|
+
rows = (0, import_utils.clamp)(1, Math.floor(containerHeight / itemHeight), rows) || rows;
|
|
200
|
+
grid.vY = rows;
|
|
201
|
+
}
|
|
202
|
+
return grid;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/hooks/react/useArrowNavigation/consts.ts
|
|
206
|
+
var import_sia_reactor4 = require("sia-reactor");
|
|
207
|
+
var H_NAV_KEYS = ["ArrowRight", "ArrowLeft", "Home", "End"];
|
|
208
|
+
var V_NAV_KEYS = ["ArrowUp", "ArrowDown", "PageDown", "PageUp"];
|
|
209
|
+
var NAV_KEYS = [...H_NAV_KEYS, ...V_NAV_KEYS];
|
|
210
|
+
var DEFAULT_CONFIG = {
|
|
211
|
+
enabled: null,
|
|
212
|
+
selector: "[data-arrow-item]",
|
|
213
|
+
focusOnHover: true,
|
|
214
|
+
loop: true,
|
|
215
|
+
virtual: false,
|
|
216
|
+
typeahead: false,
|
|
217
|
+
rovingTab: null,
|
|
218
|
+
defaultTabbableIndex: null,
|
|
219
|
+
baseTabIndex: "0",
|
|
220
|
+
resetMs: 500,
|
|
221
|
+
rtl: null,
|
|
222
|
+
grid: {},
|
|
223
|
+
activeClass: "focus-outlined",
|
|
224
|
+
inputSelector: "input,textarea,[contenteditable='true']",
|
|
225
|
+
focusOptions: { preventScroll: false },
|
|
226
|
+
scrollIntoView: { block: "nearest", inline: "nearest" },
|
|
227
|
+
onSelect: import_sia_reactor4.NOOP,
|
|
228
|
+
onFocusOut: import_sia_reactor4.NOOP
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/hooks/react/useArrowNavigation/index.ts
|
|
232
|
+
function useArrowNavigation(containerRef, config = {}) {
|
|
233
|
+
const { enabled: isEnabled, selector, focusOnHover, loop, virtual, typeahead, rovingTab, resetMs, activeClass, inputSelector, defaultTabbableIndex, baseTabIndex, grid, rtl: isRtl, focusOptions, scrollIntoView, onSelect, onFocusOut } = { ...DEFAULT_CONFIG, ...config }, [gridX, setGridX] = (0, import_react3.useState)(grid.x || 1), [gridY, setGridY] = (0, import_react3.useState)(grid.y || 1), [vGridY, setVGridY] = (0, import_react3.useState)(grid.vY || 1), [activeIndex, setActiveIndex] = (0, import_react3.useState)(-1), buffer = (0, import_react3.useRef)(""), timeout = (0, import_react3.useRef)(null), itemsRef = (0, import_react3.useRef)([]), enabled = isEnabled ?? virtual, roving = rovingTab ?? !virtual, rtl = (0, import_react3.useMemo)(() => isRtl !== null ? isRtl : getComputedStyle(containerRef.current || document.body).direction === "rtl", [containerRef, isRtl]), mutationObserverRef = (0, import_react3.useRef)(null), shouldSnub = (0, import_react3.useCallback)(() => !enabled || !containerRef.current, [enabled, containerRef]), isItemDisabled = (0, import_react3.useCallback)((el) => !el ? true : el.hasAttribute("disabled") || el.hasAttribute("aria-disabled"), []), getItems = (0, import_react3.useCallback)(() => itemsRef.current = Array.from(containerRef.current?.querySelectorAll(selector) || []), [containerRef, selector]);
|
|
234
|
+
const getAbleIndex = (0, import_react3.useCallback)(
|
|
235
|
+
(targetIndex, e = { key: "ArrowRight", ctrlKey: false }) => {
|
|
236
|
+
if (shouldSnub()) return;
|
|
237
|
+
const all = itemsRef.current;
|
|
238
|
+
if (!all.length) return null;
|
|
239
|
+
let index = targetIndex;
|
|
240
|
+
let attempts = 0;
|
|
241
|
+
while (attempts < all.length) {
|
|
242
|
+
if (!isItemDisabled(all[index])) return index;
|
|
243
|
+
index = getTargetIndex({ key: e.key, currIndex: index, gridX, gridY, vGridY, length: all.length, loop, ctrlKey: e.ctrlKey, rtl });
|
|
244
|
+
attempts++;
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
},
|
|
248
|
+
[shouldSnub, isItemDisabled, gridX, gridY, vGridY, loop, rtl]
|
|
249
|
+
);
|
|
250
|
+
const goToIndex = (0, import_react3.useCallback)(
|
|
251
|
+
(targetIndex, e = { key: "ArrowRight" }) => {
|
|
252
|
+
if (shouldSnub()) return;
|
|
253
|
+
const all = itemsRef.current, index = getAbleIndex(targetIndex, e);
|
|
254
|
+
if (index === null) return;
|
|
255
|
+
setActiveIndex(index), onSelect(all[index], e);
|
|
256
|
+
if (virtual) {
|
|
257
|
+
all[index]?.scrollIntoView(scrollIntoView);
|
|
258
|
+
containerRef.current?.setAttribute("aria-activedescendant", all[index].id || "");
|
|
259
|
+
} else all[index]?.focus(focusOptions);
|
|
260
|
+
},
|
|
261
|
+
[shouldSnub, virtual, onSelect, scrollIntoView, focusOptions, getAbleIndex, containerRef]
|
|
262
|
+
);
|
|
263
|
+
const updateDOM = (0, import_react3.useCallback)(() => {
|
|
264
|
+
if (shouldSnub() || !virtual && !roving) return;
|
|
265
|
+
const all = itemsRef.current;
|
|
266
|
+
if (!all.length) return;
|
|
267
|
+
const tabbableIndex = defaultTabbableIndex !== null && !isItemDisabled(all[defaultTabbableIndex]) ? defaultTabbableIndex : getAbleIndex(0);
|
|
268
|
+
for (let i = 0, len = all.length; i < len; i++) {
|
|
269
|
+
const el = all[i], isActive = i === activeIndex;
|
|
270
|
+
if (roving) el.setAttribute("tabindex", i === activeIndex || activeIndex === -1 && i === tabbableIndex ? baseTabIndex : "-1");
|
|
271
|
+
else if (virtual && activeIndex > 0) el.setAttribute("tabindex", i > activeIndex ? baseTabIndex : "-1");
|
|
272
|
+
else el.setAttribute("tabindex", baseTabIndex);
|
|
273
|
+
if (virtual) el.setAttribute("aria-selected", String(isActive)), el.classList.toggle(activeClass, isActive);
|
|
274
|
+
}
|
|
275
|
+
}, [shouldSnub, activeIndex, virtual, activeClass, roving, defaultTabbableIndex, getAbleIndex, isItemDisabled, baseTabIndex]);
|
|
276
|
+
const typeAhead = (0, import_react3.useCallback)(
|
|
277
|
+
(key) => {
|
|
278
|
+
if (shouldSnub() || !typeahead) return;
|
|
279
|
+
const all = itemsRef.current;
|
|
280
|
+
buffer.current += key.toLowerCase();
|
|
281
|
+
if (timeout.current) clearTimeout(timeout.current);
|
|
282
|
+
timeout.current = setTimeout(() => buffer.current = "", resetMs);
|
|
283
|
+
const start = activeIndex >= 0 ? activeIndex + 1 : 0;
|
|
284
|
+
for (let i = 0; i < all.length; i++) {
|
|
285
|
+
const idx = (start + i) % all.length, label = (all[idx].getAttribute("data-label") || all[idx].innerText || "").trim().toLowerCase();
|
|
286
|
+
if (label.startsWith(buffer.current)) return goToIndex(idx);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
[shouldSnub, typeahead, resetMs, goToIndex, activeIndex]
|
|
290
|
+
);
|
|
291
|
+
const simulateKey = (0, import_react3.useCallback)(
|
|
292
|
+
(e) => {
|
|
293
|
+
if (shouldSnub() || getActiveElement()?.matches("option")) return;
|
|
294
|
+
const all = itemsRef.current, { key } = e;
|
|
295
|
+
if (!all.length) return;
|
|
296
|
+
if (virtual && (key === " " || key === "Enter")) return all[activeIndex]?.click();
|
|
297
|
+
if (e.target?.matches(DEFAULT_CONFIG.inputSelector) && !virtual) return;
|
|
298
|
+
if (typeahead && key.length === 1 && /^[a-z0-9]$/i.test(key)) return typeAhead(key);
|
|
299
|
+
if (!NAV_KEYS.includes(key)) return;
|
|
300
|
+
if (!(e.currentTarget?.matches(DEFAULT_CONFIG.inputSelector) && gridX <= 1 && H_NAV_KEYS.includes(key))) e.preventDefault?.(), e.stopPropagation?.();
|
|
301
|
+
const currIndex = virtual ? activeIndex : all.indexOf(getActiveElement()), targetIndex = getTargetIndex({ currIndex, gridX, gridY, vGridY, length: all.length, loop, rtl, key, ctrlKey: e.ctrlKey });
|
|
302
|
+
goToIndex(targetIndex, e);
|
|
303
|
+
},
|
|
304
|
+
[shouldSnub, virtual, activeIndex, gridX, gridY, vGridY, loop, rtl, goToIndex, typeahead, typeAhead]
|
|
305
|
+
);
|
|
306
|
+
const latest = (0, import_react3.useRef)({ getItems, updateDOM, activeIndex });
|
|
307
|
+
(0, import_react3.useEffect)(() => void (latest.current = { getItems, updateDOM, activeIndex }), [getItems, updateDOM, activeIndex]);
|
|
308
|
+
(0, import_react3.useEffect)(() => void getItems(), [getItems, enabled]);
|
|
309
|
+
(0, import_react3.useEffect)(() => void (!enabled && setActiveIndex(-1)), [enabled]);
|
|
310
|
+
(0, import_react3.useEffect)(() => updateDOM(), [activeIndex, updateDOM]);
|
|
311
|
+
(0, import_react3.useEffect)(() => {
|
|
312
|
+
if (shouldSnub()) return;
|
|
313
|
+
const container = containerRef.current, handleKeyDown = simulateKey, interactiveEls = !virtual ? [container] : [container.querySelector(inputSelector)];
|
|
314
|
+
for (const el of interactiveEls) el?.addEventListener("keydown", handleKeyDown);
|
|
315
|
+
return () => {
|
|
316
|
+
for (const el of interactiveEls) el?.removeEventListener("keydown", handleKeyDown);
|
|
317
|
+
};
|
|
318
|
+
}, [shouldSnub, containerRef, virtual, inputSelector, simulateKey]);
|
|
319
|
+
(0, import_react3.useEffect)(() => {
|
|
320
|
+
if (shouldSnub()) return;
|
|
321
|
+
const container = containerRef.current;
|
|
322
|
+
const handleFocusOut = (e) => {
|
|
323
|
+
if (!container.contains(e.relatedTarget)) return setActiveIndex(-1), onFocusOut(e);
|
|
324
|
+
const among = itemsRef.current.includes(e.relatedTarget);
|
|
325
|
+
if (!among && (defaultTabbableIndex ?? -1) >= 0) return setActiveIndex(-1);
|
|
326
|
+
if (virtual) setActiveIndex(-1);
|
|
327
|
+
};
|
|
328
|
+
container.addEventListener("focusout", handleFocusOut);
|
|
329
|
+
return () => container.removeEventListener("focusout", handleFocusOut);
|
|
330
|
+
}, [shouldSnub, containerRef, onFocusOut, defaultTabbableIndex, virtual]);
|
|
331
|
+
(0, import_react3.useEffect)(() => {
|
|
332
|
+
if (!enabled || !focusOnHover) return;
|
|
333
|
+
const all = itemsRef.current;
|
|
334
|
+
const handleHover = (e) => {
|
|
335
|
+
if (!focusOnHover) return;
|
|
336
|
+
const el = e.currentTarget;
|
|
337
|
+
const i = itemsRef.current?.indexOf(el);
|
|
338
|
+
if (i !== -1) goToIndex(i);
|
|
339
|
+
};
|
|
340
|
+
for (const el of all) el.addEventListener("mouseenter", handleHover);
|
|
341
|
+
return () => {
|
|
342
|
+
for (const el of all) el.removeEventListener("mouseenter", handleHover);
|
|
343
|
+
};
|
|
344
|
+
}, [enabled, focusOnHover, goToIndex]);
|
|
345
|
+
(0, import_react3.useEffect)(() => {
|
|
346
|
+
if (shouldSnub()) return;
|
|
347
|
+
const observer = mutationObserverRef.current = new MutationObserver(() => {
|
|
348
|
+
const { getItems: getItems2, updateDOM: updateDOM2, activeIndex: activeIndex2 } = latest.current;
|
|
349
|
+
const oldEl = itemsRef.current[activeIndex2];
|
|
350
|
+
getItems2();
|
|
351
|
+
const newEl = itemsRef.current[activeIndex2];
|
|
352
|
+
updateDOM2();
|
|
353
|
+
if (oldEl && newEl && oldEl === newEl) return;
|
|
354
|
+
setActiveIndex(-1);
|
|
355
|
+
});
|
|
356
|
+
observer.observe(containerRef.current, { childList: true, subtree: true });
|
|
357
|
+
return () => observer.disconnect();
|
|
358
|
+
}, [shouldSnub, containerRef]);
|
|
359
|
+
(0, import_react3.useEffect)(() => {
|
|
360
|
+
if (shouldSnub()) return;
|
|
361
|
+
const all = itemsRef.current;
|
|
362
|
+
const setGrid = (g) => {
|
|
363
|
+
if (g.x) setGridX(g.x);
|
|
364
|
+
if (g.y) setGridY(g.y);
|
|
365
|
+
if (g.vY) setVGridY(g.vY);
|
|
366
|
+
};
|
|
367
|
+
setGrid(grid);
|
|
368
|
+
if (grid.x && grid.y && grid.vY) return;
|
|
369
|
+
const calcGrid = () => setGrid(getGrid(all, !grid.x, !grid.y, !grid.vY));
|
|
370
|
+
calcGrid();
|
|
371
|
+
const resizeObserver = new ResizeObserver(() => calcGrid());
|
|
372
|
+
resizeObserver.observe(getCommonAncestor(all[0], all[1]) ?? containerRef.current);
|
|
373
|
+
return () => resizeObserver.disconnect();
|
|
374
|
+
}, [shouldSnub, containerRef, grid]);
|
|
375
|
+
(0, import_react3.useEffect)(() => void (timeout.current && clearTimeout(timeout.current)), []);
|
|
376
|
+
return { gridX, gridY, vGridY, activeIndex, activeItem: (0, import_react3.useCallback)(() => itemsRef.current[activeIndex] ?? null, [activeIndex]), items: (0, import_react3.useCallback)(() => itemsRef.current, []), getAbleIndex, typeAhead, goToIndex, simulateKey };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/hooks/react/useFocusTrap.ts
|
|
380
|
+
var import_react4 = require("react");
|
|
381
|
+
|
|
382
|
+
// src/hooks/vanilla/focusTrap.ts
|
|
383
|
+
var import_sia_reactor5 = require("sia-reactor");
|
|
384
|
+
var stacks2 = /* @__PURE__ */ new WeakMap();
|
|
385
|
+
function initFocusTrap(el, { enabled = false, initialSelector = "[data-autofocus]", ringClassName = "focus-outline", root = window, scoped = true, capture = true } = import_sia_reactor5.NIL) {
|
|
386
|
+
const existing = (t007._ftrappers ??= /* @__PURE__ */ new WeakMap()).get(el);
|
|
387
|
+
if (!enabled || existing) return existing ? existing : void 0;
|
|
388
|
+
scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
|
|
389
|
+
const stack = stacks2.get(root) ?? [], focused = document.querySelector(":focus"), initial = el.querySelector(initialSelector), first = (0, import_utils2.createEl)("span", { tabIndex: 0 }, { focusGuard: "start" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), last = (0, import_utils2.createEl)("span", { tabIndex: 0 }, { focusGuard: "end" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), getFocusable = (c = el) => Array.prototype.filter.call(c.querySelectorAll(INTERACTIVE_SELECTOR), (el2) => !el2.hasAttribute("disabled") && !el2.hasAttribute("aria-hidden") && !el2.hasAttribute("data-focus-guard")), resetFocus = (i = 0, els = getFocusable()) => els?.length ? els.at(i).focus() : (!el.hasAttribute("tabindex") && (el.tabIndex = -1), el.focus()), edgeFocus = (pre = false) => {
|
|
390
|
+
if (!scoped) return resetFocus(pre ? -1 : 0);
|
|
391
|
+
else if (root.hasAttribute("tabindex")) return root.focus();
|
|
392
|
+
const items = getFocusable();
|
|
393
|
+
if (!items.length) return resetFocus(0, null);
|
|
394
|
+
const all = getFocusable(root.parentElement?.closest(`:has(${INTERACTIVE_SELECTOR})`) || document.body);
|
|
395
|
+
for (let target, len = all.length, i = all.indexOf(items[pre ? 0 : items.length - 1]) + (pre ? -1 : 1); pre ? i >= 0 : i < len; pre ? i-- : i++) if (!root.contains(target = all[i])) return target.focus();
|
|
396
|
+
(pre ? first : last).blur();
|
|
397
|
+
}, handleFocusIn = () => stack.at(-1) === el && getActiveElement() !== root && !el.contains(getActiveElement()) && setTimeout(resetFocus, 0, 0), handleInitialBlur = () => initial.classList.remove(ringClassName);
|
|
398
|
+
first.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus(true) : resetFocus(), capture), el.prepend(first);
|
|
399
|
+
last.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus() : resetFocus(-1), capture), el.append(last);
|
|
400
|
+
root.addEventListener("focusin", handleFocusIn, capture);
|
|
401
|
+
if (!el.querySelector(":focus")) !initial ? resetFocus() : setTimeout(() => (initial.classList.add(ringClassName), initial.focus(), initial.addEventListener("blur", handleInitialBlur, capture)));
|
|
402
|
+
if (!stack.includes(el)) stack.push(el), stacks2.set(root, stack);
|
|
403
|
+
const destroy = () => {
|
|
404
|
+
focused?.isConnected && focused.focus(), first.remove(), last.remove();
|
|
405
|
+
root.removeEventListener("focusin", handleFocusIn, capture);
|
|
406
|
+
initial?.removeEventListener("blur", handleInitialBlur, capture);
|
|
407
|
+
t007._ftrappers.delete(el), stack.splice(stack.indexOf(el), 1);
|
|
408
|
+
};
|
|
409
|
+
return t007._ftrappers.set(el, destroy), destroy;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/hooks/react/useFocusTrap.ts
|
|
413
|
+
var import_sia_reactor6 = require("sia-reactor");
|
|
414
|
+
function useFocusTrap(ref, config = import_sia_reactor6.NIL) {
|
|
415
|
+
(0, import_react4.useEffect)(() => ref.current ? initFocusTrap(ref.current, config) : void 0, [ref, config.enabled, config.initialSelector, config.ringClassName, config.root, config.scoped, config.capture]);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/hooks/react/useHighlight.ts
|
|
419
|
+
var import_react5 = require("react");
|
|
420
|
+
function useHighlight(text, query, ignoreCase = true, options) {
|
|
421
|
+
const { trimQuery = true, wholeWord = false } = options ?? {};
|
|
422
|
+
return (0, import_react5.useMemo)(() => {
|
|
423
|
+
if (!query) return [{ text, match: false }];
|
|
424
|
+
const queries = (Array.isArray(query) ? query : [query]).sort((a, b) => b.length - a.length).map((q) => (trimQuery ? q.trim() : q)?.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).filter(Boolean);
|
|
425
|
+
if (!queries.length) return [{ text, match: false }];
|
|
426
|
+
const boundary = wholeWord ? "\\b" : "", regex = new RegExp(`(${queries.map((q) => boundary + q + boundary).join("|")})`, ignoreCase ? "gi" : "g"), parts = text.split(regex);
|
|
427
|
+
return parts.map((part) => ({ text: part, match: !!part.match(regex) }));
|
|
428
|
+
}, [text, query, ignoreCase, trimQuery, wholeWord]);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/hooks/react/useRipple.ts
|
|
432
|
+
var import_react6 = require("react");
|
|
433
|
+
|
|
434
|
+
// src/hooks/vanilla/ripple.ts
|
|
435
|
+
var import_sia_reactor7 = require("sia-reactor");
|
|
436
|
+
function rippleHandler(e, { target, forceCenter = false, wrapperClassName = "t007-ripple-wrapper", className = "t007-ripple", holdClassName = "t007-ripple-hold", fadeClassName = "t007-ripple-fade" } = import_sia_reactor7.NIL) {
|
|
437
|
+
const el = target || e.currentTarget;
|
|
438
|
+
if (!el || e.target !== e.currentTarget && isInteractive(e.target) || el.hasAttribute("disabled") || e.pointerType === "mouse" && e.button !== 0) return;
|
|
439
|
+
e.stopPropagation?.();
|
|
440
|
+
const { offsetWidth: rW, offsetHeight: rH } = el, { width: w, height: h, left: l, top: t } = el.getBoundingClientRect(), size = Math.max(rW, rH), x = forceCenter ? rW / 2 - size / 2 : (e.clientX - l) * rW / w - size / 2, y = forceCenter ? rH / 2 - size / 2 : (e.clientY - t) * rH / h - size / 2, wrapper = (0, import_utils2.createEl)("span", { className: wrapperClassName }), ripple = (0, import_utils2.createEl)("span", { className: className + " " + holdClassName }, {}, { cssText: `width:${size}px;height:${size}px;left:${x}px;top:${y}px;` });
|
|
441
|
+
let canRelease = false;
|
|
442
|
+
ripple.addEventListener("animationend", () => canRelease = true, { once: true });
|
|
443
|
+
el.append(wrapper.appendChild(ripple).parentElement);
|
|
444
|
+
const release = () => {
|
|
445
|
+
if (!canRelease) return ripple.addEventListener("animationend", release, { once: true });
|
|
446
|
+
ripple.classList.replace(holdClassName, fadeClassName);
|
|
447
|
+
ripple.addEventListener("animationend", () => setTimeout(() => wrapper.remove()));
|
|
448
|
+
for (const evt of ["pointerup", "pointercancel"]) (el.ownerDocument?.defaultView || window).removeEventListener(evt, release);
|
|
449
|
+
};
|
|
450
|
+
for (const evt of ["pointerup", "pointercancel"]) (el.ownerDocument?.defaultView || window).addEventListener(evt, release);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/hooks/react/useRipple.ts
|
|
454
|
+
var import_sia_reactor8 = require("sia-reactor");
|
|
455
|
+
function useRipple() {
|
|
456
|
+
return (0, import_react6.useCallback)((e, config = import_sia_reactor8.NIL) => rippleHandler(e.nativeEvent, config), []);
|
|
457
|
+
}
|
|
458
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
459
|
+
0 && (module.exports = {
|
|
460
|
+
useArrowNavigation,
|
|
461
|
+
useFocusTrap,
|
|
462
|
+
useHighlight,
|
|
463
|
+
useOutsideClick,
|
|
464
|
+
useRipple,
|
|
465
|
+
useScrollAssist
|
|
466
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { a as ScrollAssistConfig, C as Config, K as KeyEvent } from '../scrollAssist-y9wFmYgt.cjs';
|
|
3
|
+
import { O as OutsideClickConfig, F as FocusTrapConfig, R as RippleConfig } from '../ripple-DcMWw_AP.cjs';
|
|
4
|
+
export { H as HighlightOptions, u as useHighlight } from '../useHighlight-DMpDCILK.cjs';
|
|
5
|
+
|
|
6
|
+
/** Configuration options for the `useScrollAssist` hook. */
|
|
7
|
+
interface UseScrollAssistConfig extends ScrollAssistConfig {
|
|
8
|
+
/** Enables or disables the scroll assist. Defaults to `true`. */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** React hook to add edge-scrolling assist to an element. It creates invisible "hot zones" at the edges of the element that, when hovered or dragged into, will scroll the element in that direction.
|
|
12
|
+
* The assist is automatically disabled when the element is too small or contains interactive elements near the edges to prevent interference.
|
|
13
|
+
* @param ref Ref of Scrollable element to enhance.
|
|
14
|
+
* @param options Scroll assist configuration.
|
|
15
|
+
* @returns An object with a method to manually update scroll assist visibility.
|
|
16
|
+
*/
|
|
17
|
+
declare function useScrollAssist(ref: RefObject<HTMLElement>, { enabled, pxPerSecond, assistClassName, vertical, horizontal }?: UseScrollAssistConfig): {
|
|
18
|
+
update: () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** React hook to attach outside-click, escape, and optional focus-out handling to an element. */
|
|
22
|
+
declare function useOutsideClick<T extends HTMLElement>(ref: RefObject<T>, config?: OutsideClickConfig): void;
|
|
23
|
+
|
|
24
|
+
/** A React hook for managing robustarrow-key roving focus navigation. */
|
|
25
|
+
declare function useArrowNavigation(containerRef: React.RefObject<HTMLElement>, config?: Config): {
|
|
26
|
+
gridX: number;
|
|
27
|
+
gridY: number;
|
|
28
|
+
vGridY: number;
|
|
29
|
+
activeIndex: number;
|
|
30
|
+
activeItem: () => HTMLElement;
|
|
31
|
+
items: () => HTMLElement[];
|
|
32
|
+
getAbleIndex: (targetIndex: number, e?: KeyEvent) => number | null | undefined;
|
|
33
|
+
typeAhead: (key: string) => void;
|
|
34
|
+
goToIndex: (targetIndex: number, e?: KeyEvent) => void;
|
|
35
|
+
simulateKey: (e: KeyEvent) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** React hook to keep focus trapped inside an element until disabled. */
|
|
39
|
+
declare function useFocusTrap(ref: RefObject<HTMLElement>, config?: FocusTrapConfig): void;
|
|
40
|
+
|
|
41
|
+
/** React hook to render and control a material-style ripple animation on an element.
|
|
42
|
+
* @returns Stable pointer handler for clickable UI surfaces.
|
|
43
|
+
*/
|
|
44
|
+
declare function useRipple(): (e: React.PointerEvent<HTMLElement>, config?: RippleConfig) => void;
|
|
45
|
+
|
|
46
|
+
export { useArrowNavigation, useFocusTrap, useOutsideClick, useRipple, useScrollAssist };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { a as ScrollAssistConfig, C as Config, K as KeyEvent } from '../scrollAssist-y9wFmYgt.js';
|
|
3
|
+
import { O as OutsideClickConfig, F as FocusTrapConfig, R as RippleConfig } from '../ripple-DcMWw_AP.js';
|
|
4
|
+
export { H as HighlightOptions, u as useHighlight } from '../useHighlight-DMpDCILK.js';
|
|
5
|
+
|
|
6
|
+
/** Configuration options for the `useScrollAssist` hook. */
|
|
7
|
+
interface UseScrollAssistConfig extends ScrollAssistConfig {
|
|
8
|
+
/** Enables or disables the scroll assist. Defaults to `true`. */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** React hook to add edge-scrolling assist to an element. It creates invisible "hot zones" at the edges of the element that, when hovered or dragged into, will scroll the element in that direction.
|
|
12
|
+
* The assist is automatically disabled when the element is too small or contains interactive elements near the edges to prevent interference.
|
|
13
|
+
* @param ref Ref of Scrollable element to enhance.
|
|
14
|
+
* @param options Scroll assist configuration.
|
|
15
|
+
* @returns An object with a method to manually update scroll assist visibility.
|
|
16
|
+
*/
|
|
17
|
+
declare function useScrollAssist(ref: RefObject<HTMLElement>, { enabled, pxPerSecond, assistClassName, vertical, horizontal }?: UseScrollAssistConfig): {
|
|
18
|
+
update: () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** React hook to attach outside-click, escape, and optional focus-out handling to an element. */
|
|
22
|
+
declare function useOutsideClick<T extends HTMLElement>(ref: RefObject<T>, config?: OutsideClickConfig): void;
|
|
23
|
+
|
|
24
|
+
/** A React hook for managing robustarrow-key roving focus navigation. */
|
|
25
|
+
declare function useArrowNavigation(containerRef: React.RefObject<HTMLElement>, config?: Config): {
|
|
26
|
+
gridX: number;
|
|
27
|
+
gridY: number;
|
|
28
|
+
vGridY: number;
|
|
29
|
+
activeIndex: number;
|
|
30
|
+
activeItem: () => HTMLElement;
|
|
31
|
+
items: () => HTMLElement[];
|
|
32
|
+
getAbleIndex: (targetIndex: number, e?: KeyEvent) => number | null | undefined;
|
|
33
|
+
typeAhead: (key: string) => void;
|
|
34
|
+
goToIndex: (targetIndex: number, e?: KeyEvent) => void;
|
|
35
|
+
simulateKey: (e: KeyEvent) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** React hook to keep focus trapped inside an element until disabled. */
|
|
39
|
+
declare function useFocusTrap(ref: RefObject<HTMLElement>, config?: FocusTrapConfig): void;
|
|
40
|
+
|
|
41
|
+
/** React hook to render and control a material-style ripple animation on an element.
|
|
42
|
+
* @returns Stable pointer handler for clickable UI surfaces.
|
|
43
|
+
*/
|
|
44
|
+
declare function useRipple(): (e: React.PointerEvent<HTMLElement>, config?: RippleConfig) => void;
|
|
45
|
+
|
|
46
|
+
export { useArrowNavigation, useFocusTrap, useOutsideClick, useRipple, useScrollAssist };
|