@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,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
|
+
};
|