@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.
@@ -0,0 +1,276 @@
1
+ import {
2
+ useHighlight
3
+ } from "../chunk-OGFBI6PS.js";
4
+ import {
5
+ DEFAULT_CONFIG,
6
+ H_NAV_KEYS,
7
+ NAV_KEYS,
8
+ getCommonAncestor,
9
+ getGrid,
10
+ getTargetIndex,
11
+ initFocusTrap,
12
+ initOutsideClick,
13
+ rippleHandler
14
+ } from "../chunk-AI5O3OGE.js";
15
+ import {
16
+ INTERACTIVE_SELECTOR,
17
+ getActiveElement
18
+ } from "../chunk-XVFFZZJA.js";
19
+
20
+ // src/hooks/react/useScrollAssist.ts
21
+ import { useEffect, useRef, useCallback } from "react";
22
+ import { NIL } from "sia-reactor";
23
+ function useScrollAssist(ref, { enabled = true, pxPerSecond = 80, assistClassName = "t007-scroll-assist", vertical = true, horizontal = true } = NIL) {
24
+ const scrollId = useRef(null), last = useRef(performance.now()), assists = useRef({}), assistWidth = useRef(20), assistHeight = useRef(20);
25
+ const update = useCallback(() => {
26
+ const el = ref.current;
27
+ if (!el) return;
28
+ const hasInteractive = !!el.querySelector(INTERACTIVE_SELECTOR);
29
+ if (horizontal) {
30
+ const w = assists.current.left?.offsetWidth || assistWidth.current;
31
+ const check = hasInteractive ? el.clientWidth < w * 2 : false;
32
+ assists.current.left.style.display = check ? "none" : el.scrollLeft > 0 ? "block" : "none";
33
+ assists.current.right.style.display = check ? "none" : el.scrollLeft + el.clientWidth < el.scrollWidth - 1 ? "block" : "none";
34
+ assistWidth.current = w;
35
+ }
36
+ if (vertical) {
37
+ const h = assists.current.up?.offsetHeight || assistHeight.current;
38
+ const check = hasInteractive ? el.clientHeight < h * 2 : false;
39
+ assists.current.up.style.display = check ? "none" : el.scrollTop > 0 ? "block" : "none";
40
+ assists.current.down.style.display = check ? "none" : el.scrollTop + el.clientHeight < el.scrollHeight - 1 ? "block" : "none";
41
+ assistHeight.current = h;
42
+ }
43
+ }, [ref, horizontal, vertical]);
44
+ const scroll = useCallback(
45
+ (dir) => {
46
+ const el = ref.current;
47
+ if (!el) return;
48
+ const loop = () => {
49
+ const now = performance.now(), dt = now - last.current;
50
+ last.current = now;
51
+ const d = pxPerSecond * dt / 1e3;
52
+ if (dir === "left") el.scrollLeft = Math.max(0, el.scrollLeft - d);
53
+ if (dir === "right") el.scrollLeft = Math.min(el.scrollWidth - el.clientWidth, el.scrollLeft + d);
54
+ if (dir === "up") el.scrollTop = Math.max(0, el.scrollTop - d);
55
+ if (dir === "down") el.scrollTop = Math.min(el.scrollHeight - el.clientHeight, el.scrollTop + d);
56
+ scrollId.current = requestAnimationFrame(loop);
57
+ };
58
+ last.current = performance.now();
59
+ loop();
60
+ },
61
+ [ref, pxPerSecond]
62
+ );
63
+ const stop = useCallback(() => void (cancelAnimationFrame(scrollId.current ?? 0), scrollId.current = null), []);
64
+ const createAssist = useCallback(
65
+ (dir) => {
66
+ const el = document.createElement("div");
67
+ el.className = assistClassName;
68
+ el.dataset.scrollDirection = dir;
69
+ el.style.display = "none";
70
+ for (const evt of ["pointerenter", "dragenter"]) el.addEventListener(evt, () => scroll(dir));
71
+ for (const evt of ["pointerleave", "pointerup", "pointercancel", "dragleave", "dragend"]) el.addEventListener(evt, stop);
72
+ return el;
73
+ },
74
+ [assistClassName, scroll, stop]
75
+ );
76
+ useEffect(() => {
77
+ if (!enabled) return;
78
+ const el = ref.current;
79
+ if (!el || !el.parentElement) return;
80
+ const parent = el.parentElement;
81
+ const dirs = [...horizontal ? ["left", "right"] : [], ...vertical ? ["up", "down"] : []];
82
+ for (const dir of dirs) {
83
+ const div = createAssist(dir);
84
+ assists.current[dir] = div;
85
+ (dir === "left" || dir === "up" ? parent.insertBefore : parent.appendChild).call(parent, div, el);
86
+ }
87
+ const observer = new ResizeObserver(update), mObserver = new MutationObserver(update);
88
+ update(), el.addEventListener("scroll", update);
89
+ observer.observe(el), mObserver.observe(el, { childList: true, subtree: true, characterData: true });
90
+ const allAssists = assists.current;
91
+ return () => {
92
+ stop(), el.removeEventListener("scroll", update);
93
+ observer.disconnect(), mObserver.disconnect();
94
+ for (const a of Object.values(allAssists)) a?.remove();
95
+ };
96
+ }, [ref, enabled, assistClassName, vertical, horizontal, createAssist, update, stop]);
97
+ return { update };
98
+ }
99
+
100
+ // src/hooks/react/useOutsideClick.ts
101
+ import { useEffect as useEffect2 } from "react";
102
+ import { NIL as NIL2 } from "sia-reactor";
103
+ function useOutsideClick(ref, config = NIL2) {
104
+ useEffect2(() => 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]);
105
+ }
106
+
107
+ // src/hooks/react/useArrowNavigation/index.ts
108
+ import { useEffect as useEffect3, useState, useCallback as useCallback2, useRef as useRef2, useMemo } from "react";
109
+ function useArrowNavigation(containerRef, config = {}) {
110
+ 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] = useState(grid.x || 1), [gridY, setGridY] = useState(grid.y || 1), [vGridY, setVGridY] = useState(grid.vY || 1), [activeIndex, setActiveIndex] = useState(-1), buffer = useRef2(""), timeout = useRef2(null), itemsRef = useRef2([]), enabled = isEnabled ?? virtual, roving = rovingTab ?? !virtual, rtl = useMemo(() => isRtl !== null ? isRtl : getComputedStyle(containerRef.current || document.body).direction === "rtl", [containerRef, isRtl]), mutationObserverRef = useRef2(null), shouldSnub = useCallback2(() => !enabled || !containerRef.current, [enabled, containerRef]), isItemDisabled = useCallback2((el) => !el ? true : el.hasAttribute("disabled") || el.hasAttribute("aria-disabled"), []), getItems = useCallback2(() => itemsRef.current = Array.from(containerRef.current?.querySelectorAll(selector) || []), [containerRef, selector]);
111
+ const getAbleIndex = useCallback2(
112
+ (targetIndex, e = { key: "ArrowRight", ctrlKey: false }) => {
113
+ if (shouldSnub()) return;
114
+ const all = itemsRef.current;
115
+ if (!all.length) return null;
116
+ let index = targetIndex;
117
+ let attempts = 0;
118
+ while (attempts < all.length) {
119
+ if (!isItemDisabled(all[index])) return index;
120
+ index = getTargetIndex({ key: e.key, currIndex: index, gridX, gridY, vGridY, length: all.length, loop, ctrlKey: e.ctrlKey, rtl });
121
+ attempts++;
122
+ }
123
+ return null;
124
+ },
125
+ [shouldSnub, isItemDisabled, gridX, gridY, vGridY, loop, rtl]
126
+ );
127
+ const goToIndex = useCallback2(
128
+ (targetIndex, e = { key: "ArrowRight" }) => {
129
+ if (shouldSnub()) return;
130
+ const all = itemsRef.current, index = getAbleIndex(targetIndex, e);
131
+ if (index === null) return;
132
+ setActiveIndex(index), onSelect(all[index], e);
133
+ if (virtual) {
134
+ all[index]?.scrollIntoView(scrollIntoView);
135
+ containerRef.current?.setAttribute("aria-activedescendant", all[index].id || "");
136
+ } else all[index]?.focus(focusOptions);
137
+ },
138
+ [shouldSnub, virtual, onSelect, scrollIntoView, focusOptions, getAbleIndex, containerRef]
139
+ );
140
+ const updateDOM = useCallback2(() => {
141
+ if (shouldSnub() || !virtual && !roving) return;
142
+ const all = itemsRef.current;
143
+ if (!all.length) return;
144
+ const tabbableIndex = defaultTabbableIndex !== null && !isItemDisabled(all[defaultTabbableIndex]) ? defaultTabbableIndex : getAbleIndex(0);
145
+ for (let i = 0, len = all.length; i < len; i++) {
146
+ const el = all[i], isActive = i === activeIndex;
147
+ if (roving) el.setAttribute("tabindex", i === activeIndex || activeIndex === -1 && i === tabbableIndex ? baseTabIndex : "-1");
148
+ else if (virtual && activeIndex > 0) el.setAttribute("tabindex", i > activeIndex ? baseTabIndex : "-1");
149
+ else el.setAttribute("tabindex", baseTabIndex);
150
+ if (virtual) el.setAttribute("aria-selected", String(isActive)), el.classList.toggle(activeClass, isActive);
151
+ }
152
+ }, [shouldSnub, activeIndex, virtual, activeClass, roving, defaultTabbableIndex, getAbleIndex, isItemDisabled, baseTabIndex]);
153
+ const typeAhead = useCallback2(
154
+ (key) => {
155
+ if (shouldSnub() || !typeahead) return;
156
+ const all = itemsRef.current;
157
+ buffer.current += key.toLowerCase();
158
+ if (timeout.current) clearTimeout(timeout.current);
159
+ timeout.current = setTimeout(() => buffer.current = "", resetMs);
160
+ const start = activeIndex >= 0 ? activeIndex + 1 : 0;
161
+ for (let i = 0; i < all.length; i++) {
162
+ const idx = (start + i) % all.length, label = (all[idx].getAttribute("data-label") || all[idx].innerText || "").trim().toLowerCase();
163
+ if (label.startsWith(buffer.current)) return goToIndex(idx);
164
+ }
165
+ },
166
+ [shouldSnub, typeahead, resetMs, goToIndex, activeIndex]
167
+ );
168
+ const simulateKey = useCallback2(
169
+ (e) => {
170
+ if (shouldSnub() || getActiveElement()?.matches("option")) return;
171
+ const all = itemsRef.current, { key } = e;
172
+ if (!all.length) return;
173
+ if (virtual && (key === " " || key === "Enter")) return all[activeIndex]?.click();
174
+ if (e.target?.matches(DEFAULT_CONFIG.inputSelector) && !virtual) return;
175
+ if (typeahead && key.length === 1 && /^[a-z0-9]$/i.test(key)) return typeAhead(key);
176
+ if (!NAV_KEYS.includes(key)) return;
177
+ if (!(e.currentTarget?.matches(DEFAULT_CONFIG.inputSelector) && gridX <= 1 && H_NAV_KEYS.includes(key))) e.preventDefault?.(), e.stopPropagation?.();
178
+ const currIndex = virtual ? activeIndex : all.indexOf(getActiveElement()), targetIndex = getTargetIndex({ currIndex, gridX, gridY, vGridY, length: all.length, loop, rtl, key, ctrlKey: e.ctrlKey });
179
+ goToIndex(targetIndex, e);
180
+ },
181
+ [shouldSnub, virtual, activeIndex, gridX, gridY, vGridY, loop, rtl, goToIndex, typeahead, typeAhead]
182
+ );
183
+ const latest = useRef2({ getItems, updateDOM, activeIndex });
184
+ useEffect3(() => void (latest.current = { getItems, updateDOM, activeIndex }), [getItems, updateDOM, activeIndex]);
185
+ useEffect3(() => void getItems(), [getItems, enabled]);
186
+ useEffect3(() => void (!enabled && setActiveIndex(-1)), [enabled]);
187
+ useEffect3(() => updateDOM(), [activeIndex, updateDOM]);
188
+ useEffect3(() => {
189
+ if (shouldSnub()) return;
190
+ const container = containerRef.current, handleKeyDown = simulateKey, interactiveEls = !virtual ? [container] : [container.querySelector(inputSelector)];
191
+ for (const el of interactiveEls) el?.addEventListener("keydown", handleKeyDown);
192
+ return () => {
193
+ for (const el of interactiveEls) el?.removeEventListener("keydown", handleKeyDown);
194
+ };
195
+ }, [shouldSnub, containerRef, virtual, inputSelector, simulateKey]);
196
+ useEffect3(() => {
197
+ if (shouldSnub()) return;
198
+ const container = containerRef.current;
199
+ const handleFocusOut = (e) => {
200
+ if (!container.contains(e.relatedTarget)) return setActiveIndex(-1), onFocusOut(e);
201
+ const among = itemsRef.current.includes(e.relatedTarget);
202
+ if (!among && (defaultTabbableIndex ?? -1) >= 0) return setActiveIndex(-1);
203
+ if (virtual) setActiveIndex(-1);
204
+ };
205
+ container.addEventListener("focusout", handleFocusOut);
206
+ return () => container.removeEventListener("focusout", handleFocusOut);
207
+ }, [shouldSnub, containerRef, onFocusOut, defaultTabbableIndex, virtual]);
208
+ useEffect3(() => {
209
+ if (!enabled || !focusOnHover) return;
210
+ const all = itemsRef.current;
211
+ const handleHover = (e) => {
212
+ if (!focusOnHover) return;
213
+ const el = e.currentTarget;
214
+ const i = itemsRef.current?.indexOf(el);
215
+ if (i !== -1) goToIndex(i);
216
+ };
217
+ for (const el of all) el.addEventListener("mouseenter", handleHover);
218
+ return () => {
219
+ for (const el of all) el.removeEventListener("mouseenter", handleHover);
220
+ };
221
+ }, [enabled, focusOnHover, goToIndex]);
222
+ useEffect3(() => {
223
+ if (shouldSnub()) return;
224
+ const observer = mutationObserverRef.current = new MutationObserver(() => {
225
+ const { getItems: getItems2, updateDOM: updateDOM2, activeIndex: activeIndex2 } = latest.current;
226
+ const oldEl = itemsRef.current[activeIndex2];
227
+ getItems2();
228
+ const newEl = itemsRef.current[activeIndex2];
229
+ updateDOM2();
230
+ if (oldEl && newEl && oldEl === newEl) return;
231
+ setActiveIndex(-1);
232
+ });
233
+ observer.observe(containerRef.current, { childList: true, subtree: true });
234
+ return () => observer.disconnect();
235
+ }, [shouldSnub, containerRef]);
236
+ useEffect3(() => {
237
+ if (shouldSnub()) return;
238
+ const all = itemsRef.current;
239
+ const setGrid = (g) => {
240
+ if (g.x) setGridX(g.x);
241
+ if (g.y) setGridY(g.y);
242
+ if (g.vY) setVGridY(g.vY);
243
+ };
244
+ setGrid(grid);
245
+ if (grid.x && grid.y && grid.vY) return;
246
+ const calcGrid = () => setGrid(getGrid(all, !grid.x, !grid.y, !grid.vY));
247
+ calcGrid();
248
+ const resizeObserver = new ResizeObserver(() => calcGrid());
249
+ resizeObserver.observe(getCommonAncestor(all[0], all[1]) ?? containerRef.current);
250
+ return () => resizeObserver.disconnect();
251
+ }, [shouldSnub, containerRef, grid]);
252
+ useEffect3(() => void (timeout.current && clearTimeout(timeout.current)), []);
253
+ return { gridX, gridY, vGridY, activeIndex, activeItem: useCallback2(() => itemsRef.current[activeIndex] ?? null, [activeIndex]), items: useCallback2(() => itemsRef.current, []), getAbleIndex, typeAhead, goToIndex, simulateKey };
254
+ }
255
+
256
+ // src/hooks/react/useFocusTrap.ts
257
+ import { useEffect as useEffect4 } from "react";
258
+ import { NIL as NIL3 } from "sia-reactor";
259
+ function useFocusTrap(ref, config = NIL3) {
260
+ useEffect4(() => ref.current ? initFocusTrap(ref.current, config) : void 0, [ref, config.enabled, config.initialSelector, config.ringClassName, config.root, config.scoped, config.capture]);
261
+ }
262
+
263
+ // src/hooks/react/useRipple.ts
264
+ import { useCallback as useCallback3 } from "react";
265
+ import { NIL as NIL4 } from "sia-reactor";
266
+ function useRipple() {
267
+ return useCallback3((e, config = NIL4) => rippleHandler(e.nativeEvent, config), []);
268
+ }
269
+ export {
270
+ useArrowNavigation,
271
+ useFocusTrap,
272
+ useHighlight,
273
+ useOutsideClick,
274
+ useRipple,
275
+ useScrollAssist
276
+ };