@t007/utils 0.0.34 → 0.0.35

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.
@@ -1,4 +1,4 @@
1
- import { K as KeyEvent, C as Config } from './scrollAssist-l0V2H-jx.js';
1
+ import { K as KeyEvent, C as Config } from './scrollAssist-BNvPnsJq.js';
2
2
 
3
3
  type ArrowNavigationHandle = {
4
4
  /** Current computed horizontal grid size. */
@@ -21,11 +21,14 @@ type ArrowNavigationHandle = {
21
21
  goToIndex: (index: number, e?: KeyEvent) => void;
22
22
  /** Simulate directional key navigation with a keyboard-like event. */
23
23
  simulateKey: (e: KeyEvent) => void;
24
+ /** Synchronize the navigation items list with the DOM. */
25
+ sync: () => void;
24
26
  /** Remove listeners/observers and release resources. */
25
27
  destroy: () => void;
26
28
  };
27
29
  /** A vanilla JavaScript utility for managing robust arrow-key roving focus navigation. */
28
30
  declare function initArrowNavigation(container: HTMLElement, config?: Config): ArrowNavigationHandle | void;
29
31
  declare const removeArrowNavigation: (container: HTMLElement) => void | undefined;
32
+ declare const syncArrowNavigation: (container: HTMLElement) => void | undefined;
30
33
 
31
- export { type ArrowNavigationHandle as A, initArrowNavigation as i, removeArrowNavigation as r };
34
+ export { type ArrowNavigationHandle as A, initArrowNavigation as i, removeArrowNavigation as r, syncArrowNavigation as s };
@@ -1,4 +1,4 @@
1
- import { K as KeyEvent, C as Config } from './scrollAssist-l0V2H-jx.cjs';
1
+ import { K as KeyEvent, C as Config } from './scrollAssist-BNvPnsJq.cjs';
2
2
 
3
3
  type ArrowNavigationHandle = {
4
4
  /** Current computed horizontal grid size. */
@@ -21,11 +21,14 @@ type ArrowNavigationHandle = {
21
21
  goToIndex: (index: number, e?: KeyEvent) => void;
22
22
  /** Simulate directional key navigation with a keyboard-like event. */
23
23
  simulateKey: (e: KeyEvent) => void;
24
+ /** Synchronize the navigation items list with the DOM. */
25
+ sync: () => void;
24
26
  /** Remove listeners/observers and release resources. */
25
27
  destroy: () => void;
26
28
  };
27
29
  /** A vanilla JavaScript utility for managing robust arrow-key roving focus navigation. */
28
30
  declare function initArrowNavigation(container: HTMLElement, config?: Config): ArrowNavigationHandle | void;
29
31
  declare const removeArrowNavigation: (container: HTMLElement) => void | undefined;
32
+ declare const syncArrowNavigation: (container: HTMLElement) => void | undefined;
30
33
 
31
- export { type ArrowNavigationHandle as A, initArrowNavigation as i, removeArrowNavigation as r };
34
+ export { type ArrowNavigationHandle as A, initArrowNavigation as i, removeArrowNavigation as r, syncArrowNavigation as s };
@@ -4,39 +4,39 @@ import {
4
4
  createEl,
5
5
  getActiveEl,
6
6
  isInteractive
7
- } from "./chunk-HGUYOV3P.js";
7
+ } from "./chunk-ZALPHCSJ.js";
8
8
 
9
9
  // src/ts/hooks/vanilla/outsideClick.ts
10
10
  import { NIL, NOOP } from "sia-reactor";
11
11
  function initOutsideClick(el, { enabled = false, onOutside = NOOP, outOnClick = true, outOnEscape = true, outOnFocusOut = false, allowBounds = false, allowInputs = false, root = window, scoped = true, capture = true } = NIL) {
12
- const stacks = t007._outsiders_stacks ??= /* @__PURE__ */ new WeakMap(), existing = (t007._outsiders ??= /* @__PURE__ */ new WeakMap()).get(el);
12
+ const stack = t007._outsiders_stack ??= [], existing = (t007._outsiders ??= /* @__PURE__ */ new WeakMap()).get(el);
13
13
  if (!enabled || existing) return existing ? existing : void 0;
14
14
  scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
15
- const stack = stacks.get(root) ?? [], onScopedOut = (e, t, p = e.touches?.[0] || e, rect = allowBounds ? el.getBoundingClientRect() : null) => {
15
+ const onScopedOut = (e, t, p = e.touches?.[0] || e, rect = allowBounds ? el.getBoundingClientRect() : null) => {
16
16
  if (stack.at(-1) !== el || (allowBounds ? p.clientX >= rect.left && p.clientX <= rect.right && p.clientY >= rect.top && p.clientY <= rect.bottom : el.contains(t))) return false;
17
17
  return (!scoped ? true : root.contains(t)) && onOutside(e);
18
18
  }, handleClick = ((e) => outOnClick && !(allowInputs && isInteractive(e.target)) && onScopedOut(e, e.target)), handleEscape = ((e) => outOnEscape && e.key === "Escape" && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && stack.at(-1) === el && onOutside(e)), handleFocusOut = (e) => outOnFocusOut && !el.contains(e.relatedTarget) && onScopedOut(e, e.relatedTarget);
19
19
  root.addEventListener("mousedown", handleClick, capture), root.addEventListener("touchstart", handleClick, { passive: true, capture });
20
20
  root.addEventListener("keydown", handleEscape, capture);
21
21
  el.addEventListener("focusout", handleFocusOut, capture);
22
- if (!stack.includes(el)) stack.push(el), stacks.set(root, stack);
23
22
  const destroy = () => {
24
23
  root.removeEventListener("mousedown", handleClick, capture), root.removeEventListener("touchstart", handleClick, capture);
25
24
  root.removeEventListener("keydown", handleEscape, capture);
26
25
  el.removeEventListener("focusout", handleFocusOut, capture);
27
26
  t007._outsiders.delete(el), stack.splice(stack.indexOf(el), 1);
28
27
  };
29
- return t007._outsiders.set(el, destroy), destroy;
28
+ return !stack.includes(el) && stack.push(el), t007._outsiders.set(el, destroy), destroy;
30
29
  }
31
30
  var removeOutsideClick = (el) => t007._outsiders?.get(el)?.();
32
31
 
33
32
  // src/ts/hooks/vanilla/focusTrap.ts
34
33
  import { NIL as NIL2 } from "sia-reactor";
35
34
  function initFocusTrap(el, { enabled = false, initialSelector = "[data-autofocus]", ringClassName = "focus-outline", root = window, scoped = true, capture = true } = NIL2) {
36
- const stacks = t007._ftrappers_stacks ??= /* @__PURE__ */ new WeakMap(), existing = (t007._ftrappers ??= /* @__PURE__ */ new WeakMap()).get(el);
35
+ const stack = t007._ftrappers_stack ??= [], existing = (t007._ftrappers ??= /* @__PURE__ */ new WeakMap()).get(el);
37
36
  if (!enabled || existing) return existing ? existing : void 0;
38
37
  scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
39
- const stack = stacks.get(root) ?? [], focused = document.querySelector(":focus"), initial = el.querySelector(initialSelector), first = createEl("span", { tabIndex: 0 }, { focusGuard: "start" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), last = createEl("span", { tabIndex: 0 }, { focusGuard: "end" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), getFocusable = (c = el) => [...c.querySelectorAll(INTERACTIVE_SELECTOR)], resetFocus = (i = 0, els = getFocusable()) => els?.length ? els.at(i).focus() : (!el.hasAttribute("tabindex") && (el.tabIndex = -1), el.focus()), edgeFocus = (pre = false, rt = root) => {
38
+ let recovering = false;
39
+ const focused = document.querySelector(":focus"), initial = el.querySelector(initialSelector), first = createEl("span", { tabIndex: 0 }, { focusGuard: "start" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), last = createEl("span", { tabIndex: 0 }, { focusGuard: "end" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), getFocusable = (c = el) => [...c.querySelectorAll(INTERACTIVE_SELECTOR)], resetFocus = (i = 0, els = getFocusable()) => !recovering && (recovering = true, els?.length ? els.at(i).focus() : (!el.hasAttribute("tabindex") && (el.tabIndex = -1), el.focus()), recovering = false), edgeFocus = (pre = false, rt = root) => {
40
40
  if (!scoped) return resetFocus(pre ? -1 : 0);
41
41
  if (rt.hasAttribute("tabindex")) return rt.focus();
42
42
  const items = getFocusable();
@@ -46,24 +46,25 @@ function initFocusTrap(el, { enabled = false, initialSelector = "[data-autofocus
46
46
  while (p !== ceil && (!all.length || (pre ? rt.contains(all[0]) : rt.contains(all.at(-1))))) all = getFocusable(p = p.parentElement || ceil);
47
47
  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 (!rt.contains(target = all[i])) return target.focus();
48
48
  (pre ? first : last).blur();
49
- }, handleFocusIn = () => {
49
+ }, handleFocusIn = (e) => {
50
50
  if (document.querySelector("dialog:modal") && !el.matches("dialog:modal")) return;
51
- stack.at(-1) === el && getActiveEl(el.ownerDocument) !== root && !el.contains(getActiveEl(el.ownerDocument)) && resetFocus();
51
+ stack.at(-1) === el && getActiveEl(el.ownerDocument) !== root && !el.contains(getActiveEl(el.ownerDocument)) && !e.composedPath().includes(el) && resetFocus();
52
52
  }, handleInitialBlur = () => initial.classList.remove(ringClassName);
53
53
  first.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus(true) : resetFocus(), capture), el.prepend(first);
54
54
  last.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus() : resetFocus(-1), capture), el.append(last);
55
55
  root.addEventListener("focusin", handleFocusIn, capture);
56
56
  if (initial || !el.contains(focused)) !initial ? setTimeout(resetFocus) : setTimeout(() => (initial.classList.add(ringClassName), initial.focus(), initial.addEventListener("blur", handleInitialBlur, capture)));
57
- if (!stack.includes(el)) stack.push(el), stacks.set(root, stack);
58
57
  const destroy = () => {
59
58
  focused?.isConnected && focused.focus(), first.remove(), last.remove();
60
59
  root.removeEventListener("focusin", handleFocusIn, capture);
61
60
  initial?.removeEventListener("blur", handleInitialBlur, capture);
62
61
  t007._ftrappers.delete(el), stack.splice(stack.indexOf(el), 1);
63
62
  };
64
- return t007._ftrappers.set(el, destroy), destroy;
63
+ const handle = { destroy, sync: () => (el.prepend(first), el.append(last)) };
64
+ return !stack.includes(el) && stack.push(el), t007._ftrappers.set(el, handle), handle;
65
65
  }
66
- var removeFocusTrap = (el) => t007._ftrappers?.get(el)?.();
66
+ var removeFocusTrap = (el) => t007._ftrappers?.get(el)?.destroy();
67
+ var syncFocusTrap = (el) => t007._ftrappers?.get(el)?.sync();
67
68
 
68
69
  // src/ts/hooks/vanilla/ripple.ts
69
70
  import { NIL as NIL3 } from "sia-reactor";
@@ -168,5 +169,6 @@ export {
168
169
  DEFAULT_CONFIG,
169
170
  initFocusTrap,
170
171
  removeFocusTrap,
172
+ syncFocusTrap,
171
173
  rippleHandler
172
174
  };
@@ -35,8 +35,12 @@ function loadResource(req, type = "style", { module, media, crossOrigin, integri
35
35
  function getWindow(el = window) {
36
36
  return el?.ownerDocument?.defaultView ?? el?.defaultView ?? window;
37
37
  }
38
- function createListRenderer({ container, getKey, createNode, updateNode = NOOP, destroyNode = NOOP }) {
38
+ function createListRenderer({ container, getKey, createNode, updateNode = NOOP, destroyNode = NOOP, initNode }) {
39
39
  let nodeRegistry = /* @__PURE__ */ new Map();
40
+ if (initNode) {
41
+ const children = Array.from(container.children);
42
+ for (let i = 0, len = children.length; i < len; i++) initNode(children[i], (key) => nodeRegistry.set(key, children[i]));
43
+ }
40
44
  return function syncDOM(array, strict = true) {
41
45
  const newRegistry = /* @__PURE__ */ new Map(), seenKeys = /* @__PURE__ */ new Set(), oldPositions = /* @__PURE__ */ new WeakMap(), children = Array.from(container.children);
42
46
  for (let i = 0, len = children.length; i < len; i++) oldPositions.set(children[i], i);
@@ -202,12 +206,13 @@ function RAFLoop(key, fn, signal, win) {
202
206
  }
203
207
  var cancelRAFLoop = (key) => t007._RAFLoopers ? t007._RAFLoopers.delete(key) : false;
204
208
  function limited(FN_KEY, fn, opts = {}) {
205
- let count = 0, { key, maxTimes: max = 1 } = isStr(opts) ? { key: opts } : opts;
209
+ let count = 0, { key, maxTimes: max = 1, oncePerSession = true } = isStr(opts) ? { key: opts } : opts;
206
210
  const getReg = () => JSON.parse(localStorage.getItem(FN_KEY) || "{}"), setReg = (r) => localStorage.setItem(FN_KEY, JSON.stringify(r));
207
211
  const handle = (...args) => {
212
+ if (oncePerSession && count > 0) return void 0;
208
213
  if (!key) return count++ < max ? fn(...args) : void 0;
209
214
  const r = getReg(), c = r[key] || 0;
210
- return c < max ? (r[key] = c + 1, setReg(r), fn(...args)) : void 0;
215
+ return c < max ? (count++, r[key] = c + 1, setReg(r), fn(...args)) : void 0;
211
216
  };
212
217
  handle.left = max - (handle.count = count);
213
218
  handle.reset = () => (count = 0, key && ((r) => (delete r[key], setReg(r)))(getReg()));
@@ -127,24 +127,23 @@ var import_react2 = require("react");
127
127
  // src/ts/hooks/vanilla/outsideClick.ts
128
128
  var import_sia_reactor3 = require("sia-reactor");
129
129
  function initOutsideClick(el, { enabled = false, onOutside = import_sia_reactor3.NOOP, outOnClick = true, outOnEscape = true, outOnFocusOut = false, allowBounds = false, allowInputs = false, root = window, scoped = true, capture = true } = import_sia_reactor3.NIL) {
130
- const stacks = t007._outsiders_stacks ??= /* @__PURE__ */ new WeakMap(), existing = (t007._outsiders ??= /* @__PURE__ */ new WeakMap()).get(el);
130
+ const stack = t007._outsiders_stack ??= [], existing = (t007._outsiders ??= /* @__PURE__ */ new WeakMap()).get(el);
131
131
  if (!enabled || existing) return existing ? existing : void 0;
132
132
  scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
133
- const stack = stacks.get(root) ?? [], onScopedOut = (e, t, p = e.touches?.[0] || e, rect = allowBounds ? el.getBoundingClientRect() : null) => {
133
+ const onScopedOut = (e, t, p = e.touches?.[0] || e, rect = allowBounds ? el.getBoundingClientRect() : null) => {
134
134
  if (stack.at(-1) !== el || (allowBounds ? p.clientX >= rect.left && p.clientX <= rect.right && p.clientY >= rect.top && p.clientY <= rect.bottom : el.contains(t))) return false;
135
135
  return (!scoped ? true : root.contains(t)) && onOutside(e);
136
136
  }, handleClick = ((e) => outOnClick && !(allowInputs && isInteractive(e.target)) && onScopedOut(e, e.target)), handleEscape = ((e) => outOnEscape && e.key === "Escape" && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && stack.at(-1) === el && onOutside(e)), handleFocusOut = (e) => outOnFocusOut && !el.contains(e.relatedTarget) && onScopedOut(e, e.relatedTarget);
137
137
  root.addEventListener("mousedown", handleClick, capture), root.addEventListener("touchstart", handleClick, { passive: true, capture });
138
138
  root.addEventListener("keydown", handleEscape, capture);
139
139
  el.addEventListener("focusout", handleFocusOut, capture);
140
- if (!stack.includes(el)) stack.push(el), stacks.set(root, stack);
141
140
  const destroy = () => {
142
141
  root.removeEventListener("mousedown", handleClick, capture), root.removeEventListener("touchstart", handleClick, capture);
143
142
  root.removeEventListener("keydown", handleEscape, capture);
144
143
  el.removeEventListener("focusout", handleFocusOut, capture);
145
144
  t007._outsiders.delete(el), stack.splice(stack.indexOf(el), 1);
146
145
  };
147
- return t007._outsiders.set(el, destroy), destroy;
146
+ return !stack.includes(el) && stack.push(el), t007._outsiders.set(el, destroy), destroy;
148
147
  }
149
148
 
150
149
  // src/ts/hooks/react/useOutsideClick.ts
@@ -228,7 +227,7 @@ var DEFAULT_CONFIG = {
228
227
 
229
228
  // src/ts/hooks/react/useArrowNavigation/index.ts
230
229
  function useArrowNavigation(containerRef, config = {}) {
231
- 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 ?? ("undefined" === typeof document ? false : 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]);
230
+ 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 ?? ("undefined" === typeof document ? false : 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 || el.matches('[disabled],[aria-disabled="true"],[inert],:disabled'), []), getItems = (0, import_react3.useCallback)(() => itemsRef.current = Array.from(containerRef.current?.querySelectorAll(selector) || []), [containerRef, selector]);
232
231
  const getAbleIndex = (0, import_react3.useCallback)(
233
232
  (targetIndex, e = { key: "ArrowRight", ctrlKey: false }) => {
234
233
  if (shouldSnub()) return;
@@ -302,8 +301,9 @@ function useArrowNavigation(containerRef, config = {}) {
302
301
  },
303
302
  [shouldSnub, virtual, activeIndex, gridX, gridY, vGridY, loop, rtl, goToIndex, typeahead, typeAhead]
304
303
  );
305
- const latest = (0, import_react3.useRef)({ getItems, updateDOM, activeIndex });
306
- (0, import_react3.useEffect)(() => void (latest.current = { getItems, updateDOM, activeIndex }), [getItems, updateDOM, activeIndex]);
304
+ const latest = (0, import_react3.useRef)({ getItems, updateDOM, activeIndex, calcGrid: () => {
305
+ } });
306
+ (0, import_react3.useEffect)(() => void (latest.current = { ...latest.current, getItems, updateDOM, activeIndex }), [getItems, updateDOM, activeIndex]);
307
307
  (0, import_react3.useEffect)(() => void getItems(), [getItems, enabled]);
308
308
  (0, import_react3.useEffect)(() => void (!enabled && setActiveIndex(-1)), [enabled]);
309
309
  (0, import_react3.useEffect)(() => updateDOM(), [activeIndex, updateDOM]);
@@ -332,8 +332,7 @@ function useArrowNavigation(containerRef, config = {}) {
332
332
  const all = itemsRef.current;
333
333
  const handleHover = (e) => {
334
334
  if (!focusOnHover) return;
335
- const el = e.currentTarget;
336
- const i = itemsRef.current?.indexOf(el);
335
+ const el = e.currentTarget, i = itemsRef.current?.indexOf(el);
337
336
  if (i !== -1) goToIndex(i);
338
337
  };
339
338
  for (const el of all) el.addEventListener("mouseenter", handleHover);
@@ -341,23 +340,23 @@ function useArrowNavigation(containerRef, config = {}) {
341
340
  for (const el of all) el.removeEventListener("mouseenter", handleHover);
342
341
  };
343
342
  }, [enabled, focusOnHover, goToIndex]);
343
+ const sync = (0, import_react3.useCallback)(() => {
344
+ const { getItems: getItems2, updateDOM: updateDOM2, activeIndex: activeIndex2 } = latest.current;
345
+ const oldEl = itemsRef.current[activeIndex2];
346
+ getItems2();
347
+ const newEl = itemsRef.current[activeIndex2];
348
+ updateDOM2();
349
+ if (oldEl && newEl && oldEl === newEl) return;
350
+ setActiveIndex(-1);
351
+ }, []);
344
352
  (0, import_react3.useEffect)(() => {
345
353
  if (shouldSnub()) return;
346
- const observer = mutationObserverRef.current = new MutationObserver(() => {
347
- const { getItems: getItems2, updateDOM: updateDOM2, activeIndex: activeIndex2 } = latest.current;
348
- const oldEl = itemsRef.current[activeIndex2];
349
- getItems2();
350
- const newEl = itemsRef.current[activeIndex2];
351
- updateDOM2();
352
- if (oldEl && newEl && oldEl === newEl) return;
353
- setActiveIndex(-1);
354
- });
354
+ const observer = mutationObserverRef.current = new MutationObserver(sync);
355
355
  observer.observe(containerRef.current, { childList: true, subtree: true });
356
356
  return () => observer.disconnect();
357
- }, [shouldSnub, containerRef]);
357
+ }, [shouldSnub, containerRef, sync]);
358
358
  (0, import_react3.useEffect)(() => {
359
359
  if (shouldSnub()) return;
360
- const all = itemsRef.current;
361
360
  const setGrid = (g) => {
362
361
  if (g.x) setGridX(g.x);
363
362
  if (g.y) setGridY(g.y);
@@ -365,14 +364,19 @@ function useArrowNavigation(containerRef, config = {}) {
365
364
  };
366
365
  setGrid(grid);
367
366
  if (grid.x && grid.y && grid.vY) return;
368
- const calcGrid = () => setGrid(getGrid(all, !grid.x, !grid.y, !grid.vY));
369
- calcGrid();
367
+ let ancestor = null;
370
368
  const resizeObserver = new ResizeObserver(() => calcGrid());
371
- resizeObserver.observe(getCommonAncestor(all[0], all[1]) ?? containerRef.current);
369
+ const calcGrid = () => {
370
+ setGrid(getGrid(itemsRef.current, !grid.x, !grid.y, !grid.vY));
371
+ const next = itemsRef.current.length > 1 ? getCommonAncestor(itemsRef.current[0], itemsRef.current[1]) : containerRef.current;
372
+ if (next && next !== ancestor) ancestor && resizeObserver.unobserve(ancestor), ancestor = next, resizeObserver.observe(ancestor);
373
+ };
374
+ latest.current.calcGrid = calcGrid;
375
+ calcGrid();
372
376
  return () => resizeObserver.disconnect();
373
377
  }, [shouldSnub, containerRef, grid]);
374
378
  (0, import_react3.useEffect)(() => void (timeout.current && clearTimeout(timeout.current)), []);
375
- 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 };
379
+ 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, sync };
376
380
  }
377
381
 
378
382
  // src/ts/hooks/react/useFocusTrap.ts
@@ -381,10 +385,11 @@ var import_react4 = require("react");
381
385
  // src/ts/hooks/vanilla/focusTrap.ts
382
386
  var import_sia_reactor6 = require("sia-reactor");
383
387
  function initFocusTrap(el, { enabled = false, initialSelector = "[data-autofocus]", ringClassName = "focus-outline", root = window, scoped = true, capture = true } = import_sia_reactor6.NIL) {
384
- const stacks = t007._ftrappers_stacks ??= /* @__PURE__ */ new WeakMap(), existing = (t007._ftrappers ??= /* @__PURE__ */ new WeakMap()).get(el);
388
+ const stack = t007._ftrappers_stack ??= [], existing = (t007._ftrappers ??= /* @__PURE__ */ new WeakMap()).get(el);
385
389
  if (!enabled || existing) return existing ? existing : void 0;
386
390
  scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
387
- const stack = stacks.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) => [...c.querySelectorAll(INTERACTIVE_SELECTOR)], resetFocus = (i = 0, els = getFocusable()) => els?.length ? els.at(i).focus() : (!el.hasAttribute("tabindex") && (el.tabIndex = -1), el.focus()), edgeFocus = (pre = false, rt = root) => {
391
+ let recovering = false;
392
+ const 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) => [...c.querySelectorAll(INTERACTIVE_SELECTOR)], resetFocus = (i = 0, els = getFocusable()) => !recovering && (recovering = true, els?.length ? els.at(i).focus() : (!el.hasAttribute("tabindex") && (el.tabIndex = -1), el.focus()), recovering = false), edgeFocus = (pre = false, rt = root) => {
388
393
  if (!scoped) return resetFocus(pre ? -1 : 0);
389
394
  if (rt.hasAttribute("tabindex")) return rt.focus();
390
395
  const items = getFocusable();
@@ -394,28 +399,33 @@ function initFocusTrap(el, { enabled = false, initialSelector = "[data-autofocus
394
399
  while (p !== ceil && (!all.length || (pre ? rt.contains(all[0]) : rt.contains(all.at(-1))))) all = getFocusable(p = p.parentElement || ceil);
395
400
  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 (!rt.contains(target = all[i])) return target.focus();
396
401
  (pre ? first : last).blur();
397
- }, handleFocusIn = () => {
402
+ }, handleFocusIn = (e) => {
398
403
  if (document.querySelector("dialog:modal") && !el.matches("dialog:modal")) return;
399
- stack.at(-1) === el && (0, import_utils3.getActiveEl)(el.ownerDocument) !== root && !el.contains((0, import_utils3.getActiveEl)(el.ownerDocument)) && resetFocus();
404
+ stack.at(-1) === el && (0, import_utils3.getActiveEl)(el.ownerDocument) !== root && !el.contains((0, import_utils3.getActiveEl)(el.ownerDocument)) && !e.composedPath().includes(el) && resetFocus();
400
405
  }, handleInitialBlur = () => initial.classList.remove(ringClassName);
401
406
  first.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus(true) : resetFocus(), capture), el.prepend(first);
402
407
  last.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus() : resetFocus(-1), capture), el.append(last);
403
408
  root.addEventListener("focusin", handleFocusIn, capture);
404
409
  if (initial || !el.contains(focused)) !initial ? setTimeout(resetFocus) : setTimeout(() => (initial.classList.add(ringClassName), initial.focus(), initial.addEventListener("blur", handleInitialBlur, capture)));
405
- if (!stack.includes(el)) stack.push(el), stacks.set(root, stack);
406
410
  const destroy = () => {
407
411
  focused?.isConnected && focused.focus(), first.remove(), last.remove();
408
412
  root.removeEventListener("focusin", handleFocusIn, capture);
409
413
  initial?.removeEventListener("blur", handleInitialBlur, capture);
410
414
  t007._ftrappers.delete(el), stack.splice(stack.indexOf(el), 1);
411
415
  };
412
- return t007._ftrappers.set(el, destroy), destroy;
416
+ const handle = { destroy, sync: () => (el.prepend(first), el.append(last)) };
417
+ return !stack.includes(el) && stack.push(el), t007._ftrappers.set(el, handle), handle;
413
418
  }
414
419
 
415
420
  // src/ts/hooks/react/useFocusTrap.ts
416
421
  var import_sia_reactor7 = require("sia-reactor");
417
422
  function useFocusTrap(ref, config = import_sia_reactor7.NIL) {
418
- (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]);
423
+ const handle = (0, import_react4.useRef)(void 0);
424
+ (0, import_react4.useEffect)(() => {
425
+ if (!ref.current) return;
426
+ return handle.current = initFocusTrap(ref.current, config), () => handle.current?.destroy();
427
+ }, [ref, config.enabled, config.initialSelector, config.ringClassName, config.root, config.scoped, config.capture]);
428
+ return { sync: () => handle.current?.sync() };
419
429
  }
420
430
 
421
431
  // src/ts/hooks/react/useHighlight.ts
@@ -1,6 +1,6 @@
1
1
  import { RefObject } from 'react';
2
- import { a as ScrollAssistConfig, C as Config, K as KeyEvent } from '../scrollAssist-l0V2H-jx.cjs';
3
- import { O as OutsideClickConfig, F as FocusTrapConfig, R as RippleConfig } from '../ripple-87VO-U7A.cjs';
2
+ import { a as ScrollAssistConfig, C as Config, K as KeyEvent, b as FocusTrapConfig } from '../scrollAssist-BNvPnsJq.cjs';
3
+ import { O as OutsideClickConfig, R as RippleConfig } from '../ripple-D1kGErTn.cjs';
4
4
  export { H as HighlightOptions, u as useHighlight } from '../useHighlight-DYc_GF0R.cjs';
5
5
 
6
6
  /** Configuration options for the `useScrollAssist` hook. */
@@ -33,10 +33,13 @@ declare function useArrowNavigation(containerRef: React.RefObject<HTMLElement>,
33
33
  typeAhead: (key: string) => void;
34
34
  goToIndex: (targetIndex: number, e?: KeyEvent) => void;
35
35
  simulateKey: (e: KeyEvent) => void;
36
+ sync: () => void;
36
37
  };
37
38
 
38
- /** React hook to keep focus trapped inside an element until disabled. */
39
- declare function useFocusTrap(ref: RefObject<HTMLElement>, config?: FocusTrapConfig): void;
39
+ /** React hook to keep focus trapped inside an element until disabled. */
40
+ declare function useFocusTrap(ref: RefObject<HTMLElement>, config?: FocusTrapConfig): {
41
+ sync: () => void | undefined;
42
+ };
40
43
 
41
44
  /** React hook to render and control a material-style ripple animation on an element.
42
45
  * @returns Stable pointer handler for clickable UI surfaces.
@@ -1,6 +1,6 @@
1
1
  import { RefObject } from 'react';
2
- import { a as ScrollAssistConfig, C as Config, K as KeyEvent } from '../scrollAssist-l0V2H-jx.js';
3
- import { O as OutsideClickConfig, F as FocusTrapConfig, R as RippleConfig } from '../ripple-87VO-U7A.js';
2
+ import { a as ScrollAssistConfig, C as Config, K as KeyEvent, b as FocusTrapConfig } from '../scrollAssist-BNvPnsJq.js';
3
+ import { O as OutsideClickConfig, R as RippleConfig } from '../ripple-D1kGErTn.js';
4
4
  export { H as HighlightOptions, u as useHighlight } from '../useHighlight-DYc_GF0R.js';
5
5
 
6
6
  /** Configuration options for the `useScrollAssist` hook. */
@@ -33,10 +33,13 @@ declare function useArrowNavigation(containerRef: React.RefObject<HTMLElement>,
33
33
  typeAhead: (key: string) => void;
34
34
  goToIndex: (targetIndex: number, e?: KeyEvent) => void;
35
35
  simulateKey: (e: KeyEvent) => void;
36
+ sync: () => void;
36
37
  };
37
38
 
38
- /** React hook to keep focus trapped inside an element until disabled. */
39
- declare function useFocusTrap(ref: RefObject<HTMLElement>, config?: FocusTrapConfig): void;
39
+ /** React hook to keep focus trapped inside an element until disabled. */
40
+ declare function useFocusTrap(ref: RefObject<HTMLElement>, config?: FocusTrapConfig): {
41
+ sync: () => void | undefined;
42
+ };
40
43
 
41
44
  /** React hook to render and control a material-style ripple animation on an element.
42
45
  * @returns Stable pointer handler for clickable UI surfaces.
@@ -11,11 +11,11 @@ import {
11
11
  initFocusTrap,
12
12
  initOutsideClick,
13
13
  rippleHandler
14
- } from "../chunk-ZRCBB5P4.js";
14
+ } from "../chunk-NX2LWYD6.js";
15
15
  import {
16
16
  INTERACTIVE_SELECTOR,
17
17
  getActiveEl
18
- } from "../chunk-HGUYOV3P.js";
18
+ } from "../chunk-ZALPHCSJ.js";
19
19
 
20
20
  // src/ts/hooks/react/useScrollAssist.ts
21
21
  import { useEffect, useRef, useCallback } from "react";
@@ -107,7 +107,7 @@ function useOutsideClick(ref, config = NIL2) {
107
107
  // src/ts/hooks/react/useArrowNavigation/index.ts
108
108
  import { useEffect as useEffect3, useState, useCallback as useCallback2, useRef as useRef2, useMemo } from "react";
109
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 ?? ("undefined" === typeof document ? false : 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]);
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 ?? ("undefined" === typeof document ? false : getComputedStyle(containerRef.current || document.body).direction === "rtl"), [containerRef, isRtl]), mutationObserverRef = useRef2(null), shouldSnub = useCallback2(() => !enabled || !containerRef.current, [enabled, containerRef]), isItemDisabled = useCallback2((el) => !el || el.matches('[disabled],[aria-disabled="true"],[inert],:disabled'), []), getItems = useCallback2(() => itemsRef.current = Array.from(containerRef.current?.querySelectorAll(selector) || []), [containerRef, selector]);
111
111
  const getAbleIndex = useCallback2(
112
112
  (targetIndex, e = { key: "ArrowRight", ctrlKey: false }) => {
113
113
  if (shouldSnub()) return;
@@ -181,8 +181,9 @@ function useArrowNavigation(containerRef, config = {}) {
181
181
  },
182
182
  [shouldSnub, virtual, activeIndex, gridX, gridY, vGridY, loop, rtl, goToIndex, typeahead, typeAhead]
183
183
  );
184
- const latest = useRef2({ getItems, updateDOM, activeIndex });
185
- useEffect3(() => void (latest.current = { getItems, updateDOM, activeIndex }), [getItems, updateDOM, activeIndex]);
184
+ const latest = useRef2({ getItems, updateDOM, activeIndex, calcGrid: () => {
185
+ } });
186
+ useEffect3(() => void (latest.current = { ...latest.current, getItems, updateDOM, activeIndex }), [getItems, updateDOM, activeIndex]);
186
187
  useEffect3(() => void getItems(), [getItems, enabled]);
187
188
  useEffect3(() => void (!enabled && setActiveIndex(-1)), [enabled]);
188
189
  useEffect3(() => updateDOM(), [activeIndex, updateDOM]);
@@ -211,8 +212,7 @@ function useArrowNavigation(containerRef, config = {}) {
211
212
  const all = itemsRef.current;
212
213
  const handleHover = (e) => {
213
214
  if (!focusOnHover) return;
214
- const el = e.currentTarget;
215
- const i = itemsRef.current?.indexOf(el);
215
+ const el = e.currentTarget, i = itemsRef.current?.indexOf(el);
216
216
  if (i !== -1) goToIndex(i);
217
217
  };
218
218
  for (const el of all) el.addEventListener("mouseenter", handleHover);
@@ -220,23 +220,23 @@ function useArrowNavigation(containerRef, config = {}) {
220
220
  for (const el of all) el.removeEventListener("mouseenter", handleHover);
221
221
  };
222
222
  }, [enabled, focusOnHover, goToIndex]);
223
+ const sync = useCallback2(() => {
224
+ const { getItems: getItems2, updateDOM: updateDOM2, activeIndex: activeIndex2 } = latest.current;
225
+ const oldEl = itemsRef.current[activeIndex2];
226
+ getItems2();
227
+ const newEl = itemsRef.current[activeIndex2];
228
+ updateDOM2();
229
+ if (oldEl && newEl && oldEl === newEl) return;
230
+ setActiveIndex(-1);
231
+ }, []);
223
232
  useEffect3(() => {
224
233
  if (shouldSnub()) return;
225
- const observer = mutationObserverRef.current = new MutationObserver(() => {
226
- const { getItems: getItems2, updateDOM: updateDOM2, activeIndex: activeIndex2 } = latest.current;
227
- const oldEl = itemsRef.current[activeIndex2];
228
- getItems2();
229
- const newEl = itemsRef.current[activeIndex2];
230
- updateDOM2();
231
- if (oldEl && newEl && oldEl === newEl) return;
232
- setActiveIndex(-1);
233
- });
234
+ const observer = mutationObserverRef.current = new MutationObserver(sync);
234
235
  observer.observe(containerRef.current, { childList: true, subtree: true });
235
236
  return () => observer.disconnect();
236
- }, [shouldSnub, containerRef]);
237
+ }, [shouldSnub, containerRef, sync]);
237
238
  useEffect3(() => {
238
239
  if (shouldSnub()) return;
239
- const all = itemsRef.current;
240
240
  const setGrid = (g) => {
241
241
  if (g.x) setGridX(g.x);
242
242
  if (g.y) setGridY(g.y);
@@ -244,21 +244,31 @@ function useArrowNavigation(containerRef, config = {}) {
244
244
  };
245
245
  setGrid(grid);
246
246
  if (grid.x && grid.y && grid.vY) return;
247
- const calcGrid = () => setGrid(getGrid(all, !grid.x, !grid.y, !grid.vY));
248
- calcGrid();
247
+ let ancestor = null;
249
248
  const resizeObserver = new ResizeObserver(() => calcGrid());
250
- resizeObserver.observe(getCommonAncestor(all[0], all[1]) ?? containerRef.current);
249
+ const calcGrid = () => {
250
+ setGrid(getGrid(itemsRef.current, !grid.x, !grid.y, !grid.vY));
251
+ const next = itemsRef.current.length > 1 ? getCommonAncestor(itemsRef.current[0], itemsRef.current[1]) : containerRef.current;
252
+ if (next && next !== ancestor) ancestor && resizeObserver.unobserve(ancestor), ancestor = next, resizeObserver.observe(ancestor);
253
+ };
254
+ latest.current.calcGrid = calcGrid;
255
+ calcGrid();
251
256
  return () => resizeObserver.disconnect();
252
257
  }, [shouldSnub, containerRef, grid]);
253
258
  useEffect3(() => void (timeout.current && clearTimeout(timeout.current)), []);
254
- return { gridX, gridY, vGridY, activeIndex, activeItem: useCallback2(() => itemsRef.current[activeIndex] ?? null, [activeIndex]), items: useCallback2(() => itemsRef.current, []), getAbleIndex, typeAhead, goToIndex, simulateKey };
259
+ return { gridX, gridY, vGridY, activeIndex, activeItem: useCallback2(() => itemsRef.current[activeIndex] ?? null, [activeIndex]), items: useCallback2(() => itemsRef.current, []), getAbleIndex, typeAhead, goToIndex, simulateKey, sync };
255
260
  }
256
261
 
257
262
  // src/ts/hooks/react/useFocusTrap.ts
258
- import { useEffect as useEffect4 } from "react";
263
+ import { useEffect as useEffect4, useRef as useRef3 } from "react";
259
264
  import { NIL as NIL3 } from "sia-reactor";
260
265
  function useFocusTrap(ref, config = NIL3) {
261
- useEffect4(() => ref.current ? initFocusTrap(ref.current, config) : void 0, [ref, config.enabled, config.initialSelector, config.ringClassName, config.root, config.scoped, config.capture]);
266
+ const handle = useRef3(void 0);
267
+ useEffect4(() => {
268
+ if (!ref.current) return;
269
+ return handle.current = initFocusTrap(ref.current, config), () => handle.current?.destroy();
270
+ }, [ref, config.enabled, config.initialSelector, config.ringClassName, config.root, config.scoped, config.capture]);
271
+ return { sync: () => handle.current?.sync() };
262
272
  }
263
273
 
264
274
  // src/ts/hooks/react/useRipple.ts
@@ -29,7 +29,9 @@ __export(vanilla_exports, {
29
29
  removeFocusTrap: () => removeFocusTrap,
30
30
  removeOutsideClick: () => removeOutsideClick,
31
31
  removeScrollAssist: () => removeScrollAssist,
32
- rippleHandler: () => rippleHandler
32
+ rippleHandler: () => rippleHandler,
33
+ syncArrowNavigation: () => syncArrowNavigation,
34
+ syncFocusTrap: () => syncFocusTrap
33
35
  });
34
36
  module.exports = __toCommonJS(vanilla_exports);
35
37
 
@@ -138,34 +140,34 @@ function initVScrollerator({ baseSpeed = 3, maxSpeed = 10, stepDelay = 2e3, base
138
140
  // src/ts/hooks/vanilla/outsideClick.ts
139
141
  var import_sia_reactor4 = require("sia-reactor");
140
142
  function initOutsideClick(el, { enabled = false, onOutside = import_sia_reactor4.NOOP, outOnClick = true, outOnEscape = true, outOnFocusOut = false, allowBounds = false, allowInputs = false, root = window, scoped = true, capture = true } = import_sia_reactor4.NIL) {
141
- const stacks = t007._outsiders_stacks ??= /* @__PURE__ */ new WeakMap(), existing = (t007._outsiders ??= /* @__PURE__ */ new WeakMap()).get(el);
143
+ const stack = t007._outsiders_stack ??= [], existing = (t007._outsiders ??= /* @__PURE__ */ new WeakMap()).get(el);
142
144
  if (!enabled || existing) return existing ? existing : void 0;
143
145
  scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
144
- const stack = stacks.get(root) ?? [], onScopedOut = (e, t, p = e.touches?.[0] || e, rect = allowBounds ? el.getBoundingClientRect() : null) => {
146
+ const onScopedOut = (e, t, p = e.touches?.[0] || e, rect = allowBounds ? el.getBoundingClientRect() : null) => {
145
147
  if (stack.at(-1) !== el || (allowBounds ? p.clientX >= rect.left && p.clientX <= rect.right && p.clientY >= rect.top && p.clientY <= rect.bottom : el.contains(t))) return false;
146
148
  return (!scoped ? true : root.contains(t)) && onOutside(e);
147
149
  }, handleClick = ((e) => outOnClick && !(allowInputs && isInteractive(e.target)) && onScopedOut(e, e.target)), handleEscape = ((e) => outOnEscape && e.key === "Escape" && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && stack.at(-1) === el && onOutside(e)), handleFocusOut = (e) => outOnFocusOut && !el.contains(e.relatedTarget) && onScopedOut(e, e.relatedTarget);
148
150
  root.addEventListener("mousedown", handleClick, capture), root.addEventListener("touchstart", handleClick, { passive: true, capture });
149
151
  root.addEventListener("keydown", handleEscape, capture);
150
152
  el.addEventListener("focusout", handleFocusOut, capture);
151
- if (!stack.includes(el)) stack.push(el), stacks.set(root, stack);
152
153
  const destroy = () => {
153
154
  root.removeEventListener("mousedown", handleClick, capture), root.removeEventListener("touchstart", handleClick, capture);
154
155
  root.removeEventListener("keydown", handleEscape, capture);
155
156
  el.removeEventListener("focusout", handleFocusOut, capture);
156
157
  t007._outsiders.delete(el), stack.splice(stack.indexOf(el), 1);
157
158
  };
158
- return t007._outsiders.set(el, destroy), destroy;
159
+ return !stack.includes(el) && stack.push(el), t007._outsiders.set(el, destroy), destroy;
159
160
  }
160
161
  var removeOutsideClick = (el) => t007._outsiders?.get(el)?.();
161
162
 
162
163
  // src/ts/hooks/vanilla/focusTrap.ts
163
164
  var import_sia_reactor5 = require("sia-reactor");
164
165
  function initFocusTrap(el, { enabled = false, initialSelector = "[data-autofocus]", ringClassName = "focus-outline", root = window, scoped = true, capture = true } = import_sia_reactor5.NIL) {
165
- const stacks = t007._ftrappers_stacks ??= /* @__PURE__ */ new WeakMap(), existing = (t007._ftrappers ??= /* @__PURE__ */ new WeakMap()).get(el);
166
+ const stack = t007._ftrappers_stack ??= [], existing = (t007._ftrappers ??= /* @__PURE__ */ new WeakMap()).get(el);
166
167
  if (!enabled || existing) return existing ? existing : void 0;
167
168
  scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
168
- const stack = stacks.get(root) ?? [], focused = document.querySelector(":focus"), initial = el.querySelector(initialSelector), first = (0, import_utils.createEl)("span", { tabIndex: 0 }, { focusGuard: "start" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), last = (0, import_utils.createEl)("span", { tabIndex: 0 }, { focusGuard: "end" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), getFocusable = (c = el) => [...c.querySelectorAll(INTERACTIVE_SELECTOR)], resetFocus = (i = 0, els = getFocusable()) => els?.length ? els.at(i).focus() : (!el.hasAttribute("tabindex") && (el.tabIndex = -1), el.focus()), edgeFocus = (pre = false, rt = root) => {
169
+ let recovering = false;
170
+ const focused = document.querySelector(":focus"), initial = el.querySelector(initialSelector), first = (0, import_utils.createEl)("span", { tabIndex: 0 }, { focusGuard: "start" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), last = (0, import_utils.createEl)("span", { tabIndex: 0 }, { focusGuard: "end" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), getFocusable = (c = el) => [...c.querySelectorAll(INTERACTIVE_SELECTOR)], resetFocus = (i = 0, els = getFocusable()) => !recovering && (recovering = true, els?.length ? els.at(i).focus() : (!el.hasAttribute("tabindex") && (el.tabIndex = -1), el.focus()), recovering = false), edgeFocus = (pre = false, rt = root) => {
169
171
  if (!scoped) return resetFocus(pre ? -1 : 0);
170
172
  if (rt.hasAttribute("tabindex")) return rt.focus();
171
173
  const items = getFocusable();
@@ -175,24 +177,25 @@ function initFocusTrap(el, { enabled = false, initialSelector = "[data-autofocus
175
177
  while (p !== ceil && (!all.length || (pre ? rt.contains(all[0]) : rt.contains(all.at(-1))))) all = getFocusable(p = p.parentElement || ceil);
176
178
  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 (!rt.contains(target = all[i])) return target.focus();
177
179
  (pre ? first : last).blur();
178
- }, handleFocusIn = () => {
180
+ }, handleFocusIn = (e) => {
179
181
  if (document.querySelector("dialog:modal") && !el.matches("dialog:modal")) return;
180
- stack.at(-1) === el && (0, import_utils2.getActiveEl)(el.ownerDocument) !== root && !el.contains((0, import_utils2.getActiveEl)(el.ownerDocument)) && resetFocus();
182
+ stack.at(-1) === el && (0, import_utils2.getActiveEl)(el.ownerDocument) !== root && !el.contains((0, import_utils2.getActiveEl)(el.ownerDocument)) && !e.composedPath().includes(el) && resetFocus();
181
183
  }, handleInitialBlur = () => initial.classList.remove(ringClassName);
182
184
  first.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus(true) : resetFocus(), capture), el.prepend(first);
183
185
  last.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus() : resetFocus(-1), capture), el.append(last);
184
186
  root.addEventListener("focusin", handleFocusIn, capture);
185
187
  if (initial || !el.contains(focused)) !initial ? setTimeout(resetFocus) : setTimeout(() => (initial.classList.add(ringClassName), initial.focus(), initial.addEventListener("blur", handleInitialBlur, capture)));
186
- if (!stack.includes(el)) stack.push(el), stacks.set(root, stack);
187
188
  const destroy = () => {
188
189
  focused?.isConnected && focused.focus(), first.remove(), last.remove();
189
190
  root.removeEventListener("focusin", handleFocusIn, capture);
190
191
  initial?.removeEventListener("blur", handleInitialBlur, capture);
191
192
  t007._ftrappers.delete(el), stack.splice(stack.indexOf(el), 1);
192
193
  };
193
- return t007._ftrappers.set(el, destroy), destroy;
194
+ const handle = { destroy, sync: () => (el.prepend(first), el.append(last)) };
195
+ return !stack.includes(el) && stack.push(el), t007._ftrappers.set(el, handle), handle;
194
196
  }
195
- var removeFocusTrap = (el) => t007._ftrappers?.get(el)?.();
197
+ var removeFocusTrap = (el) => t007._ftrappers?.get(el)?.destroy();
198
+ var syncFocusTrap = (el) => t007._ftrappers?.get(el)?.sync();
196
199
 
197
200
  // src/ts/hooks/react/useArrowNavigation/consts.ts
198
201
  var import_sia_reactor6 = require("sia-reactor");
@@ -270,7 +273,7 @@ function initArrowNavigation(container, config = {}) {
270
273
  if (!config.enabled || existing) return existing ? existing : void 0;
271
274
  const { enabled: isEnabled, selector, focusOnHover, loop, virtual, typeahead, resetMs, activeClass, inputSelector, defaultTabbableIndex, baseTabIndex, grid, rtl: isRtl, focusOptions, scrollIntoView, onSelect, onFocusOut, rovingTab } = { ...DEFAULT_CONFIG, ...config };
272
275
  let gridX = grid.x || 1, gridY = grid.y || 1, vGridY = grid.vY || 1, activeIndex = -1, buffer = "", timeout = null, items = [];
273
- const enabled = isEnabled ?? virtual, roving = rovingTab ?? !virtual, rtl = isRtl ?? ("undefined" === typeof document ? false : getComputedStyle(container).direction === "rtl"), shouldSnub = () => !enabled || !container, isItemDisabled = (el) => !el || el.hasAttribute("disabled") || el.hasAttribute("aria-disabled"), getItems = () => items = Array.from(container.querySelectorAll(selector));
276
+ const enabled = isEnabled ?? virtual, roving = rovingTab ?? !virtual, rtl = isRtl ?? ("undefined" === typeof document ? false : getComputedStyle(container).direction === "rtl"), shouldSnub = () => !enabled || !container, isItemDisabled = (el) => !el || el.matches('[disabled],[aria-disabled="true"],[inert],:disabled'), getItems = () => items = Array.from(container.querySelectorAll(selector));
274
277
  const getAbleIndex = (targetIndex, e = { key: "ArrowRight", ctrlKey: false }) => {
275
278
  if (shouldSnub() || !items.length) return null;
276
279
  let index = targetIndex;
@@ -345,37 +348,45 @@ function initArrowNavigation(container, config = {}) {
345
348
  if (idx !== -1) goToIndex(idx);
346
349
  };
347
350
  for (const el of items) el.addEventListener("mouseenter", handleHover);
348
- const mutationObserver = new MutationObserver(() => {
351
+ const sync = () => {
349
352
  const oldEl = items[activeIndex];
353
+ for (const el of items) el.removeEventListener("mouseenter", handleHover);
350
354
  getItems();
355
+ for (const el of items) el.addEventListener("mouseenter", handleHover);
356
+ calcGrid();
351
357
  const newEl = items[activeIndex];
352
358
  updateDOM();
353
359
  if (oldEl && newEl && oldEl === newEl) return;
354
- resetActiveIndex();
355
- updateDOM();
356
- });
360
+ resetActiveIndex(), updateDOM();
361
+ };
362
+ const mutationObserver = new MutationObserver(sync);
357
363
  mutationObserver.observe(container, { childList: true, subtree: true });
358
364
  const setGrid = (g) => {
359
365
  if (g.x !== void 0) gridX = g.x;
360
366
  if (g.y !== void 0) gridY = g.y;
361
367
  if (g.vY !== void 0) vGridY = g.vY;
362
368
  };
363
- const calcGrid = () => setGrid(getGrid(items, !grid.x, !grid.y, !grid.vY));
369
+ let ancestor = null;
370
+ const resizeObserver = new ResizeObserver(() => calcGrid());
371
+ const calcGrid = () => {
372
+ setGrid(getGrid(items, !grid.x, !grid.y, !grid.vY));
373
+ const next = items.length > 1 ? getCommonAncestor(items[0], items[1]) : container;
374
+ if (next && next !== ancestor) ancestor && resizeObserver.unobserve(ancestor), ancestor = next, resizeObserver.observe(ancestor);
375
+ };
364
376
  setGrid(grid), calcGrid();
365
- const ancestor = items.length > 1 ? getCommonAncestor(items[0], items[1]) : container;
366
- const resizeObserver = new ResizeObserver(calcGrid);
367
- if (ancestor) resizeObserver.observe(ancestor);
368
377
  const destroy = () => {
369
378
  for (const el of interactiveEls) el?.removeEventListener("keydown", simulateKey);
370
379
  container.removeEventListener("focusout", handleFocusOut);
371
380
  for (const el of items) el.removeEventListener("mouseenter", handleHover);
372
381
  mutationObserver.disconnect(), resizeObserver.disconnect();
373
382
  if (timeout) clearTimeout(timeout);
383
+ t007._arrownavs.delete(container);
374
384
  };
375
- const handle = { gridX: () => gridX, gridY: () => gridY, vGridY: () => vGridY, items: () => items, activeIndex: () => activeIndex, activeItem: () => items[activeIndex] ?? null, getAbleIndex, typeAhead, goToIndex, simulateKey, destroy };
385
+ const handle = { gridX: () => gridX, gridY: () => gridY, vGridY: () => vGridY, items: () => items, activeIndex: () => activeIndex, activeItem: () => items[activeIndex] ?? null, getAbleIndex, typeAhead, goToIndex, simulateKey, sync, destroy };
376
386
  return t007._arrownavs.set(container, handle), handle;
377
387
  }
378
388
  var removeArrowNavigation = (container) => t007._arrownavs?.get(container)?.destroy();
389
+ var syncArrowNavigation = (container) => t007._arrownavs?.get(container)?.sync();
379
390
 
380
391
  // src/ts/hooks/vanilla/ripple.ts
381
392
  var import_sia_reactor7 = require("sia-reactor");
@@ -409,5 +420,7 @@ function rippleHandler(e, { target, forceCenter = false, wrapperClassName = "t00
409
420
  removeFocusTrap,
410
421
  removeOutsideClick,
411
422
  removeScrollAssist,
412
- rippleHandler
423
+ rippleHandler,
424
+ syncArrowNavigation,
425
+ syncFocusTrap
413
426
  });
@@ -1,6 +1,6 @@
1
- export { a as ScrollAssistConfig, S as ScrollAssistHandle, b as ScrollDir, i as initScrollAssist, r as removeScrollAssist } from '../scrollAssist-l0V2H-jx.cjs';
2
- export { F as FocusTrapConfig, O as OutsideClickConfig, R as RippleConfig, i as initFocusTrap, a as initOutsideClick, r as removeFocusTrap, b as removeOutsideClick, c as rippleHandler } from '../ripple-87VO-U7A.cjs';
3
- export { A as ArrowNavigationHandle, i as initArrowNavigation, r as removeArrowNavigation } from '../arrowNavigation-IaWiNTa6.cjs';
1
+ export { b as FocusTrapConfig, F as FocusTrapHandle, a as ScrollAssistConfig, S as ScrollAssistHandle, c as ScrollDir, i as initFocusTrap, d as initScrollAssist, r as removeFocusTrap, e as removeScrollAssist, s as syncFocusTrap } from '../scrollAssist-BNvPnsJq.cjs';
2
+ export { O as OutsideClickConfig, R as RippleConfig, i as initOutsideClick, r as removeOutsideClick, a as rippleHandler } from '../ripple-D1kGErTn.cjs';
3
+ export { A as ArrowNavigationHandle, i as initArrowNavigation, r as removeArrowNavigation, s as syncArrowNavigation } from '../arrowNavigation-zEJbo8Er.cjs';
4
4
 
5
5
  /** Configuration for the vertical edge-scrolling helper. */
6
6
  interface ScrolleratorOptions {
@@ -1,6 +1,6 @@
1
- export { a as ScrollAssistConfig, S as ScrollAssistHandle, b as ScrollDir, i as initScrollAssist, r as removeScrollAssist } from '../scrollAssist-l0V2H-jx.js';
2
- export { F as FocusTrapConfig, O as OutsideClickConfig, R as RippleConfig, i as initFocusTrap, a as initOutsideClick, r as removeFocusTrap, b as removeOutsideClick, c as rippleHandler } from '../ripple-87VO-U7A.js';
3
- export { A as ArrowNavigationHandle, i as initArrowNavigation, r as removeArrowNavigation } from '../arrowNavigation-DF7ul3A3.js';
1
+ export { b as FocusTrapConfig, F as FocusTrapHandle, a as ScrollAssistConfig, S as ScrollAssistHandle, c as ScrollDir, i as initFocusTrap, d as initScrollAssist, r as removeFocusTrap, e as removeScrollAssist, s as syncFocusTrap } from '../scrollAssist-BNvPnsJq.js';
2
+ export { O as OutsideClickConfig, R as RippleConfig, i as initOutsideClick, r as removeOutsideClick, a as rippleHandler } from '../ripple-D1kGErTn.js';
3
+ export { A as ArrowNavigationHandle, i as initArrowNavigation, r as removeArrowNavigation, s as syncArrowNavigation } from '../arrowNavigation-B9bEdoqR.js';
4
4
 
5
5
  /** Configuration for the vertical edge-scrolling helper. */
6
6
  interface ScrolleratorOptions {
@@ -9,13 +9,14 @@ import {
9
9
  initOutsideClick,
10
10
  removeFocusTrap,
11
11
  removeOutsideClick,
12
- rippleHandler
13
- } from "../chunk-ZRCBB5P4.js";
12
+ rippleHandler,
13
+ syncFocusTrap
14
+ } from "../chunk-NX2LWYD6.js";
14
15
  import {
15
16
  INTERACTIVE_SELECTOR,
16
17
  createEl,
17
18
  getActiveEl
18
- } from "../chunk-HGUYOV3P.js";
19
+ } from "../chunk-ZALPHCSJ.js";
19
20
 
20
21
  // src/ts/hooks/vanilla/scrollAssist.ts
21
22
  import { NIL } from "sia-reactor";
@@ -113,7 +114,7 @@ function initArrowNavigation(container, config = {}) {
113
114
  if (!config.enabled || existing) return existing ? existing : void 0;
114
115
  const { enabled: isEnabled, selector, focusOnHover, loop, virtual, typeahead, resetMs, activeClass, inputSelector, defaultTabbableIndex, baseTabIndex, grid, rtl: isRtl, focusOptions, scrollIntoView, onSelect, onFocusOut, rovingTab } = { ...DEFAULT_CONFIG, ...config };
115
116
  let gridX = grid.x || 1, gridY = grid.y || 1, vGridY = grid.vY || 1, activeIndex = -1, buffer = "", timeout = null, items = [];
116
- const enabled = isEnabled ?? virtual, roving = rovingTab ?? !virtual, rtl = isRtl ?? ("undefined" === typeof document ? false : getComputedStyle(container).direction === "rtl"), shouldSnub = () => !enabled || !container, isItemDisabled = (el) => !el || el.hasAttribute("disabled") || el.hasAttribute("aria-disabled"), getItems = () => items = Array.from(container.querySelectorAll(selector));
117
+ const enabled = isEnabled ?? virtual, roving = rovingTab ?? !virtual, rtl = isRtl ?? ("undefined" === typeof document ? false : getComputedStyle(container).direction === "rtl"), shouldSnub = () => !enabled || !container, isItemDisabled = (el) => !el || el.matches('[disabled],[aria-disabled="true"],[inert],:disabled'), getItems = () => items = Array.from(container.querySelectorAll(selector));
117
118
  const getAbleIndex = (targetIndex, e = { key: "ArrowRight", ctrlKey: false }) => {
118
119
  if (shouldSnub() || !items.length) return null;
119
120
  let index = targetIndex;
@@ -188,37 +189,45 @@ function initArrowNavigation(container, config = {}) {
188
189
  if (idx !== -1) goToIndex(idx);
189
190
  };
190
191
  for (const el of items) el.addEventListener("mouseenter", handleHover);
191
- const mutationObserver = new MutationObserver(() => {
192
+ const sync = () => {
192
193
  const oldEl = items[activeIndex];
194
+ for (const el of items) el.removeEventListener("mouseenter", handleHover);
193
195
  getItems();
196
+ for (const el of items) el.addEventListener("mouseenter", handleHover);
197
+ calcGrid();
194
198
  const newEl = items[activeIndex];
195
199
  updateDOM();
196
200
  if (oldEl && newEl && oldEl === newEl) return;
197
- resetActiveIndex();
198
- updateDOM();
199
- });
201
+ resetActiveIndex(), updateDOM();
202
+ };
203
+ const mutationObserver = new MutationObserver(sync);
200
204
  mutationObserver.observe(container, { childList: true, subtree: true });
201
205
  const setGrid = (g) => {
202
206
  if (g.x !== void 0) gridX = g.x;
203
207
  if (g.y !== void 0) gridY = g.y;
204
208
  if (g.vY !== void 0) vGridY = g.vY;
205
209
  };
206
- const calcGrid = () => setGrid(getGrid(items, !grid.x, !grid.y, !grid.vY));
210
+ let ancestor = null;
211
+ const resizeObserver = new ResizeObserver(() => calcGrid());
212
+ const calcGrid = () => {
213
+ setGrid(getGrid(items, !grid.x, !grid.y, !grid.vY));
214
+ const next = items.length > 1 ? getCommonAncestor(items[0], items[1]) : container;
215
+ if (next && next !== ancestor) ancestor && resizeObserver.unobserve(ancestor), ancestor = next, resizeObserver.observe(ancestor);
216
+ };
207
217
  setGrid(grid), calcGrid();
208
- const ancestor = items.length > 1 ? getCommonAncestor(items[0], items[1]) : container;
209
- const resizeObserver = new ResizeObserver(calcGrid);
210
- if (ancestor) resizeObserver.observe(ancestor);
211
218
  const destroy = () => {
212
219
  for (const el of interactiveEls) el?.removeEventListener("keydown", simulateKey);
213
220
  container.removeEventListener("focusout", handleFocusOut);
214
221
  for (const el of items) el.removeEventListener("mouseenter", handleHover);
215
222
  mutationObserver.disconnect(), resizeObserver.disconnect();
216
223
  if (timeout) clearTimeout(timeout);
224
+ t007._arrownavs.delete(container);
217
225
  };
218
- const handle = { gridX: () => gridX, gridY: () => gridY, vGridY: () => vGridY, items: () => items, activeIndex: () => activeIndex, activeItem: () => items[activeIndex] ?? null, getAbleIndex, typeAhead, goToIndex, simulateKey, destroy };
226
+ const handle = { gridX: () => gridX, gridY: () => gridY, vGridY: () => vGridY, items: () => items, activeIndex: () => activeIndex, activeItem: () => items[activeIndex] ?? null, getAbleIndex, typeAhead, goToIndex, simulateKey, sync, destroy };
219
227
  return t007._arrownavs.set(container, handle), handle;
220
228
  }
221
229
  var removeArrowNavigation = (container) => t007._arrownavs?.get(container)?.destroy();
230
+ var syncArrowNavigation = (container) => t007._arrownavs?.get(container)?.sync();
222
231
  export {
223
232
  initArrowNavigation,
224
233
  initFocusTrap,
@@ -229,5 +238,7 @@ export {
229
238
  removeFocusTrap,
230
239
  removeOutsideClick,
231
240
  removeScrollAssist,
232
- rippleHandler
241
+ rippleHandler,
242
+ syncArrowNavigation,
243
+ syncFocusTrap
233
244
  };
package/dist/index.cjs CHANGED
@@ -121,8 +121,12 @@ function loadResource(req, type = "style", { module: module2, media, crossOrigin
121
121
  function getWindow(el = window) {
122
122
  return el?.ownerDocument?.defaultView ?? el?.defaultView ?? window;
123
123
  }
124
- function createListRenderer({ container, getKey, createNode, updateNode = import_sia_reactor.NOOP, destroyNode = import_sia_reactor.NOOP }) {
124
+ function createListRenderer({ container, getKey, createNode, updateNode = import_sia_reactor.NOOP, destroyNode = import_sia_reactor.NOOP, initNode }) {
125
125
  let nodeRegistry = /* @__PURE__ */ new Map();
126
+ if (initNode) {
127
+ const children = Array.from(container.children);
128
+ for (let i = 0, len = children.length; i < len; i++) initNode(children[i], (key) => nodeRegistry.set(key, children[i]));
129
+ }
126
130
  return function syncDOM(array, strict = true) {
127
131
  const newRegistry = /* @__PURE__ */ new Map(), seenKeys = /* @__PURE__ */ new Set(), oldPositions = /* @__PURE__ */ new WeakMap(), children = Array.from(container.children);
128
132
  for (let i = 0, len = children.length; i < len; i++) oldPositions.set(children[i], i);
@@ -288,12 +292,13 @@ function RAFLoop(key, fn, signal, win) {
288
292
  }
289
293
  var cancelRAFLoop = (key) => t007._RAFLoopers ? t007._RAFLoopers.delete(key) : false;
290
294
  function limited(FN_KEY, fn, opts = {}) {
291
- let count = 0, { key, maxTimes: max = 1 } = isStr(opts) ? { key: opts } : opts;
295
+ let count = 0, { key, maxTimes: max = 1, oncePerSession = true } = isStr(opts) ? { key: opts } : opts;
292
296
  const getReg = () => JSON.parse(localStorage.getItem(FN_KEY) || "{}"), setReg = (r) => localStorage.setItem(FN_KEY, JSON.stringify(r));
293
297
  const handle = (...args) => {
298
+ if (oncePerSession && count > 0) return void 0;
294
299
  if (!key) return count++ < max ? fn(...args) : void 0;
295
300
  const r = getReg(), c = r[key] || 0;
296
- return c < max ? (r[key] = c + 1, setReg(r), fn(...args)) : void 0;
301
+ return c < max ? (count++, r[key] = c + 1, setReg(r), fn(...args)) : void 0;
297
302
  };
298
303
  handle.left = max - (handle.count = count);
299
304
  handle.reset = () => (count = 0, key && ((r) => (delete r[key], setReg(r)))(getReg()));
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as ArrowNavigationHandle } from './arrowNavigation-IaWiNTa6.cjs';
2
- import { S as ScrollAssistHandle } from './scrollAssist-l0V2H-jx.cjs';
1
+ import { A as ArrowNavigationHandle } from './arrowNavigation-zEJbo8Er.cjs';
2
+ import { F as FocusTrapHandle, S as ScrollAssistHandle } from './scrollAssist-BNvPnsJq.cjs';
3
3
  export { NIL, NOOP } from 'sia-reactor';
4
4
  export { KEYS_BLOCKS, KeyStruct, KeysSettings, assignEl, bindAllMethods, clamp, cleanKeyCombo, createEl, formatKeyForDisplay, formatKeyShortcutsForDisplay, getActiveEl, getTermsForKey, guardAllMethods, guardMethod, isObj, keyEventAllowed, matchKeys, onAllMethods, parseForARIAKS, parseKeyCombo, requestAnimationFrame, setInterval, setTimeout, stringifyKeyEvent } from 'sia-reactor/utils';
5
5
 
@@ -11,12 +11,12 @@ declare global {
11
11
  _throttlers?: Map<string, number>;
12
12
  _debouncers?: Map<string, number>;
13
13
  _RAFLoopers?: Map<string, Function>;
14
- _ftrappers?: WeakMap<HTMLElement, () => void>;
14
+ _ftrappers?: WeakMap<HTMLElement, FocusTrapHandle>;
15
15
  _outsiders?: WeakMap<HTMLElement, () => void>;
16
16
  _arrownavs?: WeakMap<HTMLElement, ArrowNavigationHandle>;
17
17
  _scrollers?: WeakMap<HTMLElement, ScrollAssistHandle>;
18
- _ftrappers_stacks?: WeakMap<EventTarget, HTMLElement[]>;
19
- _outsiders_stacks?: WeakMap<EventTarget, HTMLElement[]>;
18
+ _ftrappers_stack?: HTMLElement[];
19
+ _outsiders_stack?: HTMLElement[];
20
20
  _scrollers_r_observer?: ResizeObserver;
21
21
  _scrollers_m_observer?: MutationObserver;
22
22
  }
@@ -180,8 +180,10 @@ declare const cancelRAFLoop: (key: string) => boolean;
180
180
  interface LimitedOptions {
181
181
  /** Storage key used to persist call counts. */
182
182
  key?: string;
183
- /** Maximum number of allowed calls. */
183
+ /** Maximum number of allowed calls. @default 1 */
184
184
  maxTimes?: number;
185
+ /** Only allow calling once per session, regardless of maxTimes. @default true */
186
+ oncePerSession?: boolean;
185
187
  }
186
188
  interface LimitedHandle<T extends (...args: any[]) => any> {
187
189
  /** Call the wrapped function with the original arguments. */
@@ -268,7 +270,7 @@ declare function loadResource(req: string | symbol, type?: ResourceType, { modul
268
270
  */
269
271
  declare function getWindow(el?: any): Window & typeof globalThis;
270
272
  /** Options for configuring a list renderer */
271
- type ListRendererOptions<T> = {
273
+ type ListRendererOptions<T, El extends HTMLElement = HTMLElement> = {
272
274
  /** The container element to render the list into */
273
275
  container: HTMLElement;
274
276
  /** Function to extract a unique key from each item.
@@ -280,24 +282,30 @@ type ListRendererOptions<T> = {
280
282
  * @param item The item to create a node for
281
283
  * @returns An HTMLElement representing the item, or null/undefined to skip rendering
282
284
  */
283
- createNode: (item: T) => HTMLElement | null | undefined;
285
+ createNode: (item: T) => El | null | undefined;
284
286
  /** Optional function to update an existing node with new item data, called when an item is reused.
285
287
  * @param node The existing DOM node for the item
286
288
  * @param item The new item data to update the node with
287
289
  */
288
- updateNode?: (node: HTMLElement, item: T) => void;
290
+ updateNode?: (node: El, item: T) => void;
289
291
  /** Optional function to clean up a DOM node when an item is removed, called before the node is removed from the DOM.
290
292
  * @param node The DOM node to be removed
291
293
  * @param key The unique key of the item associated with the node
292
294
  */
293
- destroyNode?: (node: HTMLElement, key: string) => void;
295
+ destroyNode?: (node: El, key: string) => void;
296
+ /** Optional function called once during initialization for each existing child node in the container.
297
+ * Allows seeding the registry with pre-existing markup so nodes are reused instead of destroyed.
298
+ * @param node The pre-existing DOM node
299
+ * @param register A callback to register the node with its corresponding item key
300
+ */
301
+ initNode?: (node: El, register: (key: string) => void) => void;
294
302
  };
295
303
  /**
296
304
  * Creates a list renderer function for efficiently updating a DOM list based on a new array of items using the L.I.S(Longest Increasing Subsequence) algorithm.
297
305
  * @param param0 The options for configuring the list renderer
298
306
  * @returns A function that synchronizes the DOM with the new array of items
299
307
  */
300
- declare function createListRenderer<T>({ container, getKey, createNode, updateNode, destroyNode }: ListRendererOptions<T>): (array: T[], strict?: boolean) => void;
308
+ declare function createListRenderer<T, El extends HTMLElement = HTMLElement>({ container, getKey, createNode, updateNode, destroyNode, initNode }: ListRendererOptions<T, El>): (array: T[], strict?: boolean) => void;
301
309
 
302
310
  /** Format a file size for display.
303
311
  * @param size Size in bytes.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as ArrowNavigationHandle } from './arrowNavigation-DF7ul3A3.js';
2
- import { S as ScrollAssistHandle } from './scrollAssist-l0V2H-jx.js';
1
+ import { A as ArrowNavigationHandle } from './arrowNavigation-B9bEdoqR.js';
2
+ import { F as FocusTrapHandle, S as ScrollAssistHandle } from './scrollAssist-BNvPnsJq.js';
3
3
  export { NIL, NOOP } from 'sia-reactor';
4
4
  export { KEYS_BLOCKS, KeyStruct, KeysSettings, assignEl, bindAllMethods, clamp, cleanKeyCombo, createEl, formatKeyForDisplay, formatKeyShortcutsForDisplay, getActiveEl, getTermsForKey, guardAllMethods, guardMethod, isObj, keyEventAllowed, matchKeys, onAllMethods, parseForARIAKS, parseKeyCombo, requestAnimationFrame, setInterval, setTimeout, stringifyKeyEvent } from 'sia-reactor/utils';
5
5
 
@@ -11,12 +11,12 @@ declare global {
11
11
  _throttlers?: Map<string, number>;
12
12
  _debouncers?: Map<string, number>;
13
13
  _RAFLoopers?: Map<string, Function>;
14
- _ftrappers?: WeakMap<HTMLElement, () => void>;
14
+ _ftrappers?: WeakMap<HTMLElement, FocusTrapHandle>;
15
15
  _outsiders?: WeakMap<HTMLElement, () => void>;
16
16
  _arrownavs?: WeakMap<HTMLElement, ArrowNavigationHandle>;
17
17
  _scrollers?: WeakMap<HTMLElement, ScrollAssistHandle>;
18
- _ftrappers_stacks?: WeakMap<EventTarget, HTMLElement[]>;
19
- _outsiders_stacks?: WeakMap<EventTarget, HTMLElement[]>;
18
+ _ftrappers_stack?: HTMLElement[];
19
+ _outsiders_stack?: HTMLElement[];
20
20
  _scrollers_r_observer?: ResizeObserver;
21
21
  _scrollers_m_observer?: MutationObserver;
22
22
  }
@@ -180,8 +180,10 @@ declare const cancelRAFLoop: (key: string) => boolean;
180
180
  interface LimitedOptions {
181
181
  /** Storage key used to persist call counts. */
182
182
  key?: string;
183
- /** Maximum number of allowed calls. */
183
+ /** Maximum number of allowed calls. @default 1 */
184
184
  maxTimes?: number;
185
+ /** Only allow calling once per session, regardless of maxTimes. @default true */
186
+ oncePerSession?: boolean;
185
187
  }
186
188
  interface LimitedHandle<T extends (...args: any[]) => any> {
187
189
  /** Call the wrapped function with the original arguments. */
@@ -268,7 +270,7 @@ declare function loadResource(req: string | symbol, type?: ResourceType, { modul
268
270
  */
269
271
  declare function getWindow(el?: any): Window & typeof globalThis;
270
272
  /** Options for configuring a list renderer */
271
- type ListRendererOptions<T> = {
273
+ type ListRendererOptions<T, El extends HTMLElement = HTMLElement> = {
272
274
  /** The container element to render the list into */
273
275
  container: HTMLElement;
274
276
  /** Function to extract a unique key from each item.
@@ -280,24 +282,30 @@ type ListRendererOptions<T> = {
280
282
  * @param item The item to create a node for
281
283
  * @returns An HTMLElement representing the item, or null/undefined to skip rendering
282
284
  */
283
- createNode: (item: T) => HTMLElement | null | undefined;
285
+ createNode: (item: T) => El | null | undefined;
284
286
  /** Optional function to update an existing node with new item data, called when an item is reused.
285
287
  * @param node The existing DOM node for the item
286
288
  * @param item The new item data to update the node with
287
289
  */
288
- updateNode?: (node: HTMLElement, item: T) => void;
290
+ updateNode?: (node: El, item: T) => void;
289
291
  /** Optional function to clean up a DOM node when an item is removed, called before the node is removed from the DOM.
290
292
  * @param node The DOM node to be removed
291
293
  * @param key The unique key of the item associated with the node
292
294
  */
293
- destroyNode?: (node: HTMLElement, key: string) => void;
295
+ destroyNode?: (node: El, key: string) => void;
296
+ /** Optional function called once during initialization for each existing child node in the container.
297
+ * Allows seeding the registry with pre-existing markup so nodes are reused instead of destroyed.
298
+ * @param node The pre-existing DOM node
299
+ * @param register A callback to register the node with its corresponding item key
300
+ */
301
+ initNode?: (node: El, register: (key: string) => void) => void;
294
302
  };
295
303
  /**
296
304
  * Creates a list renderer function for efficiently updating a DOM list based on a new array of items using the L.I.S(Longest Increasing Subsequence) algorithm.
297
305
  * @param param0 The options for configuring the list renderer
298
306
  * @returns A function that synchronizes the DOM with the new array of items
299
307
  */
300
- declare function createListRenderer<T>({ container, getKey, createNode, updateNode, destroyNode }: ListRendererOptions<T>): (array: T[], strict?: boolean) => void;
308
+ declare function createListRenderer<T, El extends HTMLElement = HTMLElement>({ container, getKey, createNode, updateNode, destroyNode, initNode }: ListRendererOptions<T, El>): (array: T[], strict?: boolean) => void;
301
309
 
302
310
  /** Format a file size for display.
303
311
  * @param size Size in bytes.
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ import {
60
60
  throttle,
61
61
  uid,
62
62
  uncamelize
63
- } from "./chunk-HGUYOV3P.js";
63
+ } from "./chunk-ZALPHCSJ.js";
64
64
  export {
65
65
  INTERACTIVE_SELECTOR,
66
66
  KEYS_BLOCKS,
@@ -25,25 +25,6 @@ declare function initOutsideClick(el: HTMLElement, { enabled, onOutside, outOnCl
25
25
  /** Remove outside-click handling from an element. */
26
26
  declare const removeOutsideClick: (el: HTMLElement) => void | undefined;
27
27
 
28
- interface FocusTrapConfig {
29
- /** Enables or disables the focus trap. @default `false`. */
30
- enabled?: boolean;
31
- /** The preferred initial focus target selector within the element, overrides whatever was focused. Try `autofocus` attribute if working with dialogs before this. @default `[data-autofocus]`. */
32
- initialSelector?: string;
33
- /** The class name for the initial focus ring since programmatic focus is not always visible, `autofocus` attribute in dialogs might work fine as an alternative. @default `"focus-outline"`. */
34
- ringClassName?: string;
35
- /** Optional root used to scope focus listeners to an element instead of the window. @default `window`. */
36
- root?: HTMLElement | Document | Window;
37
- /** Whether the focus trap is scoped to the root provided it is an HTMLElement. @default `true`. */
38
- scoped?: boolean;
39
- /** Passed down to all event listeners used. @default `true`. */
40
- capture?: boolean;
41
- }
42
- /** Hook to keep focus trapped inside an element until disabled. */
43
- declare function initFocusTrap(el: HTMLElement, { enabled, initialSelector, ringClassName, root, scoped, capture }?: FocusTrapConfig): (() => void) | void;
44
- /** Remove the focus trap guard from an element. */
45
- declare const removeFocusTrap: (el: HTMLElement) => void | undefined;
46
-
47
28
  interface RippleConfig {
48
29
  /** Optional explicit ripple host element. @default event currentTarget. */
49
30
  target?: HTMLElement | null;
@@ -71,4 +52,4 @@ interface RippleConfig {
71
52
  */
72
53
  declare function rippleHandler(e: Pick<PointerEvent, "target" | "currentTarget" | "button" | "clientX" | "clientY" | "stopPropagation">, { target, forceCenter, wrapperClassName, className, holdClassName, fadeClassName, maxDuration }?: RippleConfig): void;
73
54
 
74
- export { type FocusTrapConfig as F, type OutsideClickConfig as O, type RippleConfig as R, initOutsideClick as a, removeOutsideClick as b, rippleHandler as c, initFocusTrap as i, removeFocusTrap as r };
55
+ export { type OutsideClickConfig as O, type RippleConfig as R, rippleHandler as a, initOutsideClick as i, removeOutsideClick as r };
@@ -25,25 +25,6 @@ declare function initOutsideClick(el: HTMLElement, { enabled, onOutside, outOnCl
25
25
  /** Remove outside-click handling from an element. */
26
26
  declare const removeOutsideClick: (el: HTMLElement) => void | undefined;
27
27
 
28
- interface FocusTrapConfig {
29
- /** Enables or disables the focus trap. @default `false`. */
30
- enabled?: boolean;
31
- /** The preferred initial focus target selector within the element, overrides whatever was focused. Try `autofocus` attribute if working with dialogs before this. @default `[data-autofocus]`. */
32
- initialSelector?: string;
33
- /** The class name for the initial focus ring since programmatic focus is not always visible, `autofocus` attribute in dialogs might work fine as an alternative. @default `"focus-outline"`. */
34
- ringClassName?: string;
35
- /** Optional root used to scope focus listeners to an element instead of the window. @default `window`. */
36
- root?: HTMLElement | Document | Window;
37
- /** Whether the focus trap is scoped to the root provided it is an HTMLElement. @default `true`. */
38
- scoped?: boolean;
39
- /** Passed down to all event listeners used. @default `true`. */
40
- capture?: boolean;
41
- }
42
- /** Hook to keep focus trapped inside an element until disabled. */
43
- declare function initFocusTrap(el: HTMLElement, { enabled, initialSelector, ringClassName, root, scoped, capture }?: FocusTrapConfig): (() => void) | void;
44
- /** Remove the focus trap guard from an element. */
45
- declare const removeFocusTrap: (el: HTMLElement) => void | undefined;
46
-
47
28
  interface RippleConfig {
48
29
  /** Optional explicit ripple host element. @default event currentTarget. */
49
30
  target?: HTMLElement | null;
@@ -71,4 +52,4 @@ interface RippleConfig {
71
52
  */
72
53
  declare function rippleHandler(e: Pick<PointerEvent, "target" | "currentTarget" | "button" | "clientX" | "clientY" | "stopPropagation">, { target, forceCenter, wrapperClassName, className, holdClassName, fadeClassName, maxDuration }?: RippleConfig): void;
73
54
 
74
- export { type FocusTrapConfig as F, type OutsideClickConfig as O, type RippleConfig as R, initOutsideClick as a, removeOutsideClick as b, rippleHandler as c, initFocusTrap as i, removeFocusTrap as r };
55
+ export { type OutsideClickConfig as O, type RippleConfig as R, rippleHandler as a, initOutsideClick as i, removeOutsideClick as r };
@@ -38,6 +38,31 @@ type Config = {
38
38
  onFocusOut?: (e: FocusEvent) => void;
39
39
  };
40
40
 
41
+ interface FocusTrapConfig {
42
+ /** Enables or disables the focus trap. @default `false`. */
43
+ enabled?: boolean;
44
+ /** The preferred initial focus target selector within the element, overrides whatever was focused. Try `autofocus` attribute if working with dialogs before this. @default `[data-autofocus]`. */
45
+ initialSelector?: string;
46
+ /** The class name for the initial focus ring since programmatic focus is not always visible, `autofocus` attribute in dialogs might work fine as an alternative. @default `"focus-outline"`. */
47
+ ringClassName?: string;
48
+ /** Optional root used to scope focus listeners to an element instead of the window. @default `window`. */
49
+ root?: HTMLElement | Document | Window;
50
+ /** Whether the focus trap is scoped to the root provided it is an HTMLElement. @default `true`. */
51
+ scoped?: boolean;
52
+ /** Passed down to all event listeners used. @default `true`. */
53
+ capture?: boolean;
54
+ }
55
+ interface FocusTrapHandle {
56
+ destroy: () => void;
57
+ sync: () => void;
58
+ }
59
+ /** Hook to keep focus trapped inside an element until disabled. */
60
+ declare function initFocusTrap(el: HTMLElement, { enabled, initialSelector, ringClassName, root, scoped, capture }?: FocusTrapConfig): FocusTrapHandle | void;
61
+ /** Remove the focus trap guard from an element. */
62
+ declare const removeFocusTrap: (el: HTMLElement) => void | undefined;
63
+ /** Synchronize the focus trap guards. */
64
+ declare const syncFocusTrap: (el: HTMLElement) => void | undefined;
65
+
41
66
  type ScrollDir = "left" | "right" | "up" | "down";
42
67
  /** Scroll assist control object returned by initScrollAssist. */
43
68
  interface ScrollAssistHandle {
@@ -69,4 +94,4 @@ declare function initScrollAssist(el: HTMLElement, { pxPerSecond, assistClassNam
69
94
  */
70
95
  declare const removeScrollAssist: (el: HTMLElement) => void | undefined;
71
96
 
72
- export { type Config as C, type KeyEvent as K, type ScrollAssistHandle as S, type ScrollAssistConfig as a, type ScrollDir as b, initScrollAssist as i, removeScrollAssist as r };
97
+ export { type Config as C, type FocusTrapHandle as F, type KeyEvent as K, type ScrollAssistHandle as S, type ScrollAssistConfig as a, type FocusTrapConfig as b, type ScrollDir as c, initScrollAssist as d, removeScrollAssist as e, initFocusTrap as i, removeFocusTrap as r, syncFocusTrap as s };
@@ -38,6 +38,31 @@ type Config = {
38
38
  onFocusOut?: (e: FocusEvent) => void;
39
39
  };
40
40
 
41
+ interface FocusTrapConfig {
42
+ /** Enables or disables the focus trap. @default `false`. */
43
+ enabled?: boolean;
44
+ /** The preferred initial focus target selector within the element, overrides whatever was focused. Try `autofocus` attribute if working with dialogs before this. @default `[data-autofocus]`. */
45
+ initialSelector?: string;
46
+ /** The class name for the initial focus ring since programmatic focus is not always visible, `autofocus` attribute in dialogs might work fine as an alternative. @default `"focus-outline"`. */
47
+ ringClassName?: string;
48
+ /** Optional root used to scope focus listeners to an element instead of the window. @default `window`. */
49
+ root?: HTMLElement | Document | Window;
50
+ /** Whether the focus trap is scoped to the root provided it is an HTMLElement. @default `true`. */
51
+ scoped?: boolean;
52
+ /** Passed down to all event listeners used. @default `true`. */
53
+ capture?: boolean;
54
+ }
55
+ interface FocusTrapHandle {
56
+ destroy: () => void;
57
+ sync: () => void;
58
+ }
59
+ /** Hook to keep focus trapped inside an element until disabled. */
60
+ declare function initFocusTrap(el: HTMLElement, { enabled, initialSelector, ringClassName, root, scoped, capture }?: FocusTrapConfig): FocusTrapHandle | void;
61
+ /** Remove the focus trap guard from an element. */
62
+ declare const removeFocusTrap: (el: HTMLElement) => void | undefined;
63
+ /** Synchronize the focus trap guards. */
64
+ declare const syncFocusTrap: (el: HTMLElement) => void | undefined;
65
+
41
66
  type ScrollDir = "left" | "right" | "up" | "down";
42
67
  /** Scroll assist control object returned by initScrollAssist. */
43
68
  interface ScrollAssistHandle {
@@ -69,4 +94,4 @@ declare function initScrollAssist(el: HTMLElement, { pxPerSecond, assistClassNam
69
94
  */
70
95
  declare const removeScrollAssist: (el: HTMLElement) => void | undefined;
71
96
 
72
- export { type Config as C, type KeyEvent as K, type ScrollAssistHandle as S, type ScrollAssistConfig as a, type ScrollDir as b, initScrollAssist as i, removeScrollAssist as r };
97
+ export { type Config as C, type FocusTrapHandle as F, type KeyEvent as K, type ScrollAssistHandle as S, type ScrollAssistConfig as a, type FocusTrapConfig as b, type ScrollDir as c, initScrollAssist as d, removeScrollAssist as e, initFocusTrap as i, removeFocusTrap as r, syncFocusTrap as s };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t007/utils",
3
- "version": "0.0.34",
3
+ "version": "0.0.35",
4
4
  "description": "High-performance utilities for the t007 ecosystem.",
5
5
  "author": "Oketade Oluwatobiloba <tobioketade007@gmail.com>",
6
6
  "license": "MIT",
@@ -76,7 +76,7 @@
76
76
  "react-dom": "^18.3.1"
77
77
  },
78
78
  "dependencies": {
79
- "sia-reactor": "^0.0.35"
79
+ "sia-reactor": "^0.0.36"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "react": "^18.0.0"