@ryuzaki13/react-foundation-lib 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ import { useEffect as e, useEffectEvent as t, useLayoutEffect as n, useRef as r, useState as i } from "react";
2
+ //#region src/dom/downloadFile.ts
3
+ function a(e, t) {
4
+ let n = document.createElement("a");
5
+ n.href = t, n.download = e, n.click(), window.setTimeout(() => {
6
+ window.URL.revokeObjectURL(t);
7
+ }, 0);
8
+ }
9
+ function o(e, t) {
10
+ a(e, window.URL.createObjectURL(t));
11
+ }
12
+ function s(e, t) {
13
+ o(e, new Blob([JSON.stringify(t, null, 2)], { type: "application/json;charset=utf-8" }));
14
+ }
15
+ //#endregion
16
+ //#region src/dom/getOrCreatePortalRoot.ts
17
+ function c(e) {
18
+ if (typeof document > "u") return null;
19
+ let t = document.getElementById(e);
20
+ if (t instanceof HTMLElement) return t;
21
+ let n = document.createElement("div");
22
+ return n.id = e, document.body.append(n), n;
23
+ }
24
+ //#endregion
25
+ //#region src/dom/scroll-anchor/captureScrollAnchor.ts
26
+ var l = /* @__PURE__ */ new WeakMap();
27
+ function u(e, t) {
28
+ if (!e?.isConnected || !t?.isConnected || e === t || !e.contains(t)) return null;
29
+ l.get(e)?.release();
30
+ let n = t.getBoundingClientRect().top - e.getBoundingClientRect().top - e.clientTop + e.scrollTop, r = e.style.getPropertyValue("overflow-anchor"), i = e.style.getPropertyPriority("overflow-anchor"), a = !0;
31
+ e.style.setProperty("overflow-anchor", "none", "important");
32
+ let o = e.style.getPropertyValue("overflow-anchor"), s = e.style.getPropertyPriority("overflow-anchor"), c = () => e.style.getPropertyValue("overflow-anchor") === o && e.style.getPropertyPriority("overflow-anchor") === s, u = {
33
+ release() {
34
+ a && (a = !1, l.delete(e), c() && (r ? e.style.setProperty("overflow-anchor", r, i) : e.style.removeProperty("overflow-anchor")));
35
+ },
36
+ restore() {
37
+ if (!a) return !1;
38
+ try {
39
+ if (!e.isConnected || !t.isConnected || !e.contains(t) || !c()) return !1;
40
+ let r = t.getBoundingClientRect().top - e.getBoundingClientRect().top - e.clientTop + e.scrollTop - n;
41
+ return r !== 0 && e.scrollBy({
42
+ top: r,
43
+ behavior: "instant"
44
+ }), !0;
45
+ } finally {
46
+ u.release();
47
+ }
48
+ }
49
+ };
50
+ return l.set(e, u), u;
51
+ }
52
+ //#endregion
53
+ //#region src/dom/useClickOutside.ts
54
+ function d(t, n) {
55
+ e(() => {
56
+ let e = (e) => {
57
+ if (Array.isArray(t)) {
58
+ let r = !0;
59
+ for (let n of t) n.current && n.current.contains(e.target) && (r = !1);
60
+ r && n();
61
+ } else t.current && !t.current.contains(e.target) && n();
62
+ };
63
+ return document.addEventListener("mousedown", e), () => document.removeEventListener("mousedown", e);
64
+ }, [t, n]);
65
+ }
66
+ //#endregion
67
+ //#region src/dom/useElementHeightObserver.ts
68
+ function f(e) {
69
+ let [t, r] = i(0);
70
+ return n(() => {
71
+ let t = e.current;
72
+ if (!t) return;
73
+ let n = new ResizeObserver((e) => {
74
+ let n = e[0]?.contentRect.height ?? t.getBoundingClientRect().height;
75
+ r(n);
76
+ });
77
+ return n.observe(t), () => n.disconnect();
78
+ }, [e.current]), t;
79
+ }
80
+ //#endregion
81
+ //#region src/dom/useEscapeDismiss.ts
82
+ function p({ active: n, onDismiss: r, enabled: i = !0, documentTarget: a, containerRef: o }) {
83
+ let s = t(() => r());
84
+ e(() => {
85
+ if (!n || !i) return;
86
+ let e = a ?? (typeof document > "u" ? null : document);
87
+ if (!e) return;
88
+ let t = (t) => {
89
+ if (t.key !== "Escape") return;
90
+ let n = e.activeElement, r = o?.current;
91
+ r && n instanceof Node && n !== r && !r.contains(n) || (t.preventDefault(), s());
92
+ };
93
+ return e.addEventListener("keydown", t, !0), () => e.removeEventListener("keydown", t, !0);
94
+ }, [
95
+ n,
96
+ i,
97
+ a,
98
+ o
99
+ ]);
100
+ }
101
+ //#endregion
102
+ //#region src/dom/useOverlayFocus.ts
103
+ var m = "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]):not([disabled])";
104
+ function h({ active: n, trapFocus: i = !1, restoreFocus: a = !0, initialFocus: o = "auto", restoreFocusTarget: s, containerRef: c }) {
105
+ let l = r(null), u = c ?? l, d = r(null), f = t((e) => Array.from(e.querySelectorAll(m)).filter((e) => !e.hasAttribute("aria-hidden"))), p = t((e) => {
106
+ if (o === "container") return e;
107
+ if (typeof o == "function") return o(e) ?? e;
108
+ let t = f(e);
109
+ return t.find((e) => e.hasAttribute("autofocus") || e.hasAttribute("autoFocus")) ?? t[0] ?? e;
110
+ }), h = t(() => s?.() ?? d.current);
111
+ return e(() => {
112
+ if (!n) return;
113
+ d.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
114
+ let e = null, t = 0, r = () => {
115
+ if (e = u.current, !e) {
116
+ t = window.requestAnimationFrame(r);
117
+ return;
118
+ }
119
+ let n = p(e);
120
+ n instanceof HTMLElement && n.isConnected && n.focus({ preventScroll: !0 }), i && e.addEventListener("keydown", o);
121
+ }, o = (t) => {
122
+ if (!i || t.key !== "Tab" || !e) return;
123
+ let n = f(e);
124
+ if (n.length === 0) {
125
+ t.preventDefault(), e.focus();
126
+ return;
127
+ }
128
+ let r = n[0], a = n[n.length - 1], o = document.activeElement;
129
+ if (t.shiftKey) {
130
+ (o === r || o === e) && (t.preventDefault(), a.focus());
131
+ return;
132
+ }
133
+ o === a && (t.preventDefault(), r.focus());
134
+ };
135
+ return r(), () => {
136
+ if (window.cancelAnimationFrame(t), i && e && e.removeEventListener("keydown", o), !a) return;
137
+ let n = h();
138
+ n?.isConnected && n.focus({ preventScroll: !0 });
139
+ };
140
+ }, [
141
+ n,
142
+ i,
143
+ a,
144
+ u
145
+ ]), u;
146
+ }
147
+ //#endregion
148
+ //#region src/dom/useFocusTrap.ts
149
+ var g = (e) => h({
150
+ active: e,
151
+ trapFocus: !0,
152
+ restoreFocus: !0,
153
+ initialFocus: "auto"
154
+ });
155
+ //#endregion
156
+ //#region src/dom/useIntersectionObserver.ts
157
+ function _(t, n) {
158
+ let [r, a] = i(!1);
159
+ return e(() => {
160
+ let e = t.current;
161
+ if (!e) return;
162
+ let r = new IntersectionObserver(([e]) => {
163
+ a(e.isIntersecting);
164
+ }, n);
165
+ return r.observe(e), () => {
166
+ e && r.unobserve(e), r.disconnect();
167
+ };
168
+ }, [n, t]), { isIntersecting: r };
169
+ }
170
+ //#endregion
171
+ //#region src/dom/useIsTouchDevice.ts
172
+ var v = () => {
173
+ let [t, n] = i(!1);
174
+ return e(() => {
175
+ let e = () => {
176
+ let e = window.matchMedia("(pointer: coarse)").matches || "ontouchstart" in window || navigator.maxTouchPoints > 0;
177
+ n(e);
178
+ };
179
+ return e(), window.addEventListener("resize", e), () => {
180
+ window.removeEventListener("resize", e);
181
+ };
182
+ }, []), t;
183
+ };
184
+ //#endregion
185
+ export { p as a, u as c, s as d, a as f, h as i, c as l, _ as n, f as o, g as r, d as s, v as t, o as u };
186
+
187
+ //# sourceMappingURL=dom-RMvZy-Sx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom-RMvZy-Sx.js","names":[],"sources":["../../src/dom/downloadFile.ts","../../src/dom/getOrCreatePortalRoot.ts","../../src/dom/scroll-anchor/captureScrollAnchor.ts","../../src/dom/useClickOutside.ts","../../src/dom/useElementHeightObserver.ts","../../src/dom/useEscapeDismiss.ts","../../src/dom/useOverlayFocus.ts","../../src/dom/useFocusTrap.ts","../../src/dom/useIntersectionObserver.ts","../../src/dom/useIsTouchDevice.ts"],"sourcesContent":["export function downloadFileFromObjectURL(filename: string, objectUrl: string) {\n\tconst link = document.createElement(\"a\");\n\n\tlink.href = objectUrl;\n\tlink.download = filename;\n\tlink.click();\n\n\twindow.setTimeout(() => {\n\t\twindow.URL.revokeObjectURL(objectUrl);\n\t}, 0);\n}\n\nexport function downloadFileFromBlob(filename: string, blob: Blob) {\n\tconst objectUrl = window.URL.createObjectURL(blob);\n\tdownloadFileFromObjectURL(filename, objectUrl);\n}\n\nexport function downloadFileFromJson(filename: string, payload: unknown) {\n\tconst blob = new Blob([JSON.stringify(payload, null, 2)], { type: \"application/json;charset=utf-8\" });\n\tdownloadFileFromBlob(filename, blob);\n}\n","export function getOrCreatePortalRoot(id: string): HTMLElement | null {\n\tif (typeof document === \"undefined\") {\n\t\treturn null;\n\t}\n\n\tconst existingElement = document.getElementById(id);\n\tif (existingElement instanceof HTMLElement) {\n\t\treturn existingElement;\n\t}\n\n\tconst portalRoot = document.createElement(\"div\");\n\tportalRoot.id = id;\n\tdocument.body.append(portalRoot);\n\n\treturn portalRoot;\n}\n","import { type CapturedScrollAnchor } from \"./scrollAnchorTypes\";\n\n// Один scroll host не может одновременно компенсировать один и тот же prepend дважды.\nconst activeAnchors = new WeakMap<HTMLElement, CapturedScrollAnchor>();\n\n/**\n * Захватывает позицию сохраняемого элемента перед изменением обычного вертикального списка.\n * Вызывать только на browser event/effect boundary; restore — после DOM commit, release — при отмене.\n * Новый захват того же viewport завершает предыдущий без прокрутки.\n */\nexport function captureScrollAnchor(viewport: HTMLElement | null, anchor: HTMLElement | null): CapturedScrollAnchor | null {\n\tif (!viewport?.isConnected || !anchor?.isConnected || viewport === anchor || !viewport.contains(anchor)) return null;\n\n\tactiveAnchors.get(viewport)?.release();\n\tconst initialOffset =\n\t\tanchor.getBoundingClientRect().top - viewport.getBoundingClientRect().top - viewport.clientTop + viewport.scrollTop;\n\tconst initialStyle = viewport.style.getPropertyValue(\"overflow-anchor\");\n\tconst initialPriority = viewport.style.getPropertyPriority(\"overflow-anchor\");\n\tlet active = true;\n\n\t// Native anchoring иначе успеет компенсировать prepend до restore и приведёт к двойному сдвигу.\n\tviewport.style.setProperty(\"overflow-anchor\", \"none\", \"important\");\n\t// В браузере без этого CSS-свойства native anchoring тоже отсутствует: сравниваем фактически принятый стиль.\n\tconst ownedStyle = viewport.style.getPropertyValue(\"overflow-anchor\");\n\tconst ownedPriority = viewport.style.getPropertyPriority(\"overflow-anchor\");\n\tconst ownsStyle = () =>\n\t\tviewport.style.getPropertyValue(\"overflow-anchor\") === ownedStyle &&\n\t\tviewport.style.getPropertyPriority(\"overflow-anchor\") === ownedPriority;\n\n\tconst operation: CapturedScrollAnchor = {\n\t\trelease() {\n\t\t\tif (!active) return;\n\t\t\tactive = false;\n\t\t\tactiveAnchors.delete(viewport);\n\t\t\t// Не затираем новое решение host, если он сам изменил свойство во время ожидания.\n\t\t\tif (!ownsStyle()) return;\n\t\t\tif (initialStyle) viewport.style.setProperty(\"overflow-anchor\", initialStyle, initialPriority);\n\t\t\telse viewport.style.removeProperty(\"overflow-anchor\");\n\t\t},\n\t\trestore() {\n\t\t\tif (!active) return false;\n\t\t\ttry {\n\t\t\t\tif (!viewport.isConnected || !anchor.isConnected || !viewport.contains(anchor) || !ownsStyle()) return false;\n\t\t\t\tconst currentOffset =\n\t\t\t\t\tanchor.getBoundingClientRect().top - viewport.getBoundingClientRect().top - viewport.clientTop + viewport.scrollTop;\n\t\t\t\tconst delta = currentOffset - initialOffset;\n\t\t\t\t// Content-space delta не отменяет ручную прокрутку между запросом и commit.\n\t\t\t\t// Instant не наследует CSS smooth; горизонтальная позиция остаётся неизменной.\n\t\t\t\tif (delta !== 0) viewport.scrollBy({ top: delta, behavior: \"instant\" });\n\t\t\t\treturn true;\n\t\t\t} finally {\n\t\t\t\toperation.release();\n\t\t\t}\n\t\t}\n\t};\n\tactiveAnchors.set(viewport, operation);\n\treturn operation;\n}\n","import { RefObject, useEffect } from \"react\";\n\nexport function useClickOutside(ref: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[], handler: () => void) {\n\tuseEffect(() => {\n\t\tconst handleClickOutside = (event: MouseEvent) => {\n\t\t\tif (Array.isArray(ref)) {\n\t\t\t\tlet isOutside = true;\n\t\t\t\tfor (const r of ref) {\n\t\t\t\t\tif (r.current && r.current.contains(event.target as Node)) {\n\t\t\t\t\t\tisOutside = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (isOutside) {\n\t\t\t\t\thandler();\n\t\t\t\t}\n\t\t\t} else if (ref.current && !ref.current.contains(event.target as Node)) {\n\t\t\t\thandler();\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener(\"mousedown\", handleClickOutside);\n\t\treturn () => document.removeEventListener(\"mousedown\", handleClickOutside);\n\t}, [ref, handler]);\n}\n","import { type RefObject, useLayoutEffect, useState } from \"react\";\n\nexport function useElementHeightObserver<TElement extends HTMLElement>(elementRef: RefObject<TElement | null>) {\n\tconst [height, setHeight] = useState(0);\n\n\tuseLayoutEffect(() => {\n\t\tconst element = elementRef.current;\n\t\tif (!element) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst nextHeight = entries[0]?.contentRect.height ?? element.getBoundingClientRect().height;\n\t\t\tsetHeight(nextHeight);\n\t\t});\n\t\tobserver.observe(element);\n\n\t\treturn () => observer.disconnect();\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/refs\n\t}, [elementRef.current]);\n\n\treturn height;\n}\n","import { RefObject, useEffect, useEffectEvent } from \"react\";\n\ninterface UseEscapeDismissOptions {\n\tactive: boolean;\n\tonDismiss: () => void;\n\tenabled?: boolean;\n\tdocumentTarget?: Document | null;\n\tcontainerRef?: RefObject<HTMLElement | null>;\n}\n\n/**\n * Подписывает overlay на закрытие по клавише Escape.\n */\nexport function useEscapeDismiss({ active, onDismiss, enabled = true, documentTarget, containerRef }: UseEscapeDismissOptions) {\n\tconst dismiss = useEffectEvent(() => onDismiss());\n\n\tuseEffect(() => {\n\t\tif (!active || !enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst targetDocument = documentTarget ?? (typeof document === \"undefined\" ? null : document);\n\t\tif (!targetDocument) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst handleKeyDown = (event: KeyboardEvent) => {\n\t\t\tif (event.key !== \"Escape\" /*|| event.defaultPrevented*/) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst activeElement = targetDocument.activeElement;\n\t\t\tconst container = containerRef?.current;\n\t\t\tif (container && activeElement instanceof Node && activeElement !== container && !container.contains(activeElement)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tevent.preventDefault();\n\t\t\tdismiss();\n\t\t};\n\n\t\ttargetDocument.addEventListener(\"keydown\", handleKeyDown, true);\n\t\treturn () => targetDocument.removeEventListener(\"keydown\", handleKeyDown, true);\n\t}, [active, enabled, documentTarget, containerRef]);\n}\n","import { RefObject, useEffect, useEffectEvent, useRef } from \"react\";\n\nconst focusableElementsSelector =\n\t'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]):not([disabled])';\n\nexport interface UseOverlayFocusOptions<T extends HTMLElement> {\n\tactive: boolean;\n\ttrapFocus?: boolean;\n\trestoreFocus?: boolean;\n\tinitialFocus?: \"auto\" | \"container\" | ((container: T) => HTMLElement | null | undefined);\n\trestoreFocusTarget?: () => HTMLElement | null | undefined;\n\tcontainerRef?: RefObject<T | null>;\n}\n\n/**\n * Возвращает ref overlay-контейнера и управляет начальными/возвратными переходами фокуса.\n * Открытие и закрытие не прокручивают документ: портал может получить фокус\n * до завершения позиционирования. Пользовательский Tab по-прежнему раскрывает\n * выбранный элемент в прокручиваемом содержимом overlay.\n */\nexport function useOverlayFocus<T extends HTMLElement>({\n\tactive,\n\ttrapFocus = false,\n\trestoreFocus = true,\n\tinitialFocus = \"auto\",\n\trestoreFocusTarget,\n\tcontainerRef\n}: UseOverlayFocusOptions<T>) {\n\tconst internalOverlayRef = useRef<T | null>(null);\n\tconst overlayRef = containerRef ?? internalOverlayRef;\n\tconst previousActiveElementRef = useRef<HTMLElement | null>(null);\n\n\t/**\n\t * Собирает фокусируемые элементы overlay в актуальном DOM-порядке.\n\t */\n\tconst getFocusableElements = useEffectEvent((container: T) =>\n\t\tArray.from(container.querySelectorAll<HTMLElement>(focusableElementsSelector)).filter(\n\t\t\t(element) => !element.hasAttribute(\"aria-hidden\")\n\t\t)\n\t);\n\n\t/**\n\t * Вычисляет стартовый фокус при открытии overlay.\n\t */\n\tconst resolveInitialFocus = useEffectEvent((container: T) => {\n\t\tif (initialFocus === \"container\") {\n\t\t\treturn container;\n\t\t}\n\n\t\tif (typeof initialFocus === \"function\") {\n\t\t\treturn initialFocus(container) ?? container;\n\t\t}\n\n\t\tconst focusableElements = getFocusableElements(container);\n\t\treturn (\n\t\t\tfocusableElements.find((element) => element.hasAttribute(\"autofocus\") || element.hasAttribute(\"autoFocus\")) ??\n\t\t\tfocusableElements[0] ??\n\t\t\tcontainer\n\t\t);\n\t});\n\n\t/**\n\t * Вычисляет элемент, которому нужно вернуть фокус после закрытия overlay.\n\t */\n\tconst resolveRestoreFocusTarget = useEffectEvent(() => restoreFocusTarget?.() ?? previousActiveElementRef.current);\n\n\tuseEffect(() => {\n\t\tif (!active) {\n\t\t\treturn;\n\t\t}\n\n\t\tpreviousActiveElementRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n\t\tlet overlayElement: T | null = null;\n\t\tlet rafId = 0;\n\n\t\t/**\n\t\t * Цикл откладывает инициализацию, пока ref контейнера не будет привязан к DOM.\n\t\t */\n\t\tconst attachOverlay = () => {\n\t\t\toverlayElement = overlayRef.current;\n\t\t\tif (!overlayElement) {\n\t\t\t\trafId = window.requestAnimationFrame(attachOverlay);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst focusTarget = resolveInitialFocus(overlayElement);\n\t\t\tif (focusTarget instanceof HTMLElement && focusTarget.isConnected) {\n\t\t\t\tfocusTarget.focus({ preventScroll: true });\n\t\t\t}\n\n\t\t\tif (trapFocus) {\n\t\t\t\toverlayElement.addEventListener(\"keydown\", handleTabKey);\n\t\t\t}\n\t\t};\n\n\t\t/**\n\t\t * Зацикливает Tab внутри overlay, пока он открыт как модальный слой.\n\t\t */\n\t\tconst handleTabKey = (event: KeyboardEvent) => {\n\t\t\tif (!trapFocus || event.key !== \"Tab\" || !overlayElement) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst focusableElements = getFocusableElements(overlayElement);\n\t\t\tif (focusableElements.length === 0) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\toverlayElement.focus();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst firstElement = focusableElements[0];\n\t\t\tconst lastElement = focusableElements[focusableElements.length - 1];\n\t\t\tconst activeElement = document.activeElement;\n\n\t\t\tif (event.shiftKey) {\n\t\t\t\tif (activeElement === firstElement || activeElement === overlayElement) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tlastElement.focus();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (activeElement === lastElement) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tfirstElement.focus();\n\t\t\t}\n\t\t};\n\n\t\tattachOverlay();\n\n\t\treturn () => {\n\t\t\twindow.cancelAnimationFrame(rafId);\n\t\t\tif (trapFocus && overlayElement) {\n\t\t\t\toverlayElement.removeEventListener(\"keydown\", handleTabKey);\n\t\t\t}\n\n\t\t\tif (!restoreFocus) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst restoreTarget = resolveRestoreFocusTarget();\n\t\t\tif (restoreTarget?.isConnected) {\n\t\t\t\trestoreTarget.focus({ preventScroll: true });\n\t\t\t}\n\t\t};\n\t}, [active, trapFocus, restoreFocus, overlayRef]);\n\n\treturn overlayRef;\n}\n","import { useOverlayFocus } from \"./useOverlayFocus\";\n\n/**\n * Совместимый wrapper для overlay с фокус-ловушкой.\n */\nexport const useFocusTrap = (isOpen: boolean) => {\n\treturn useOverlayFocus<HTMLDivElement>({ active: isOpen, trapFocus: true, restoreFocus: true, initialFocus: \"auto\" });\n};\n","import { useEffect, useState } from \"react\";\n\nexport function useIntersectionObserver(ref: React.RefObject<Element | null>, options?: IntersectionObserverInit) {\n\tconst [isIntersecting, setIsIntersecting] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst el = ref.current;\n\t\tif (!el) return;\n\n\t\tconst observer = new IntersectionObserver(([entry]) => {\n\t\t\tsetIsIntersecting(entry.isIntersecting);\n\t\t}, options);\n\n\t\tobserver.observe(el);\n\n\t\treturn () => {\n\t\t\tif (el) {\n\t\t\t\tobserver.unobserve(el);\n\t\t\t}\n\t\t\tobserver.disconnect();\n\t\t};\n\t}, [options, ref]); // не забыть, что ref.current может меняться\n\n\treturn { isIntersecting };\n}\n","import { useEffect, useState } from \"react\";\n\nexport const useIsTouchDevice = () => {\n\tconst [isTouchDevice, setIsTouchDevice] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst checkTouchDevice = () => {\n\t\t\tconst isTouch = window.matchMedia(\"(pointer: coarse)\").matches || \"ontouchstart\" in window || navigator.maxTouchPoints > 0;\n\t\t\tsetIsTouchDevice(isTouch);\n\t\t};\n\n\t\tcheckTouchDevice();\n\n\t\t// Обработчик для изменений в типе устройства\n\t\twindow.addEventListener(\"resize\", checkTouchDevice);\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"resize\", checkTouchDevice);\n\t\t};\n\t}, []);\n\n\treturn isTouchDevice;\n};\n"],"mappings":"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"}
@@ -1,5 +1,7 @@
1
1
  export * from "./downloadFile";
2
2
  export * from "./getOrCreatePortalRoot";
3
+ export { captureScrollAnchor } from "./scroll-anchor/captureScrollAnchor";
4
+ export { type CapturedScrollAnchor } from "./scroll-anchor/scrollAnchorTypes";
3
5
  export * from "./useClickOutside";
4
6
  export * from "./useElementHeightObserver";
5
7
  export * from "./useEscapeDismiss";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dom/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dom/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
package/dist/dom/index.js CHANGED
@@ -1,159 +1,2 @@
1
- import { useEffect as e, useEffectEvent as t, useLayoutEffect as n, useRef as r, useState as i } from "react";
2
- //#region src/dom/downloadFile.ts
3
- function a(e, t) {
4
- let n = document.createElement("a");
5
- n.href = t, n.download = e, n.click(), window.setTimeout(() => {
6
- window.URL.revokeObjectURL(t);
7
- }, 0);
8
- }
9
- function o(e, t) {
10
- a(e, window.URL.createObjectURL(t));
11
- }
12
- function s(e, t) {
13
- o(e, new Blob([JSON.stringify(t, null, 2)], { type: "application/json;charset=utf-8" }));
14
- }
15
- //#endregion
16
- //#region src/dom/getOrCreatePortalRoot.ts
17
- function c(e) {
18
- if (typeof document > "u") return null;
19
- let t = document.getElementById(e);
20
- if (t instanceof HTMLElement) return t;
21
- let n = document.createElement("div");
22
- return n.id = e, document.body.append(n), n;
23
- }
24
- //#endregion
25
- //#region src/dom/useClickOutside.ts
26
- function l(t, n) {
27
- e(() => {
28
- let e = (e) => {
29
- if (Array.isArray(t)) {
30
- let r = !0;
31
- for (let n of t) n.current && n.current.contains(e.target) && (r = !1);
32
- r && n();
33
- } else t.current && !t.current.contains(e.target) && n();
34
- };
35
- return document.addEventListener("mousedown", e), () => document.removeEventListener("mousedown", e);
36
- }, [t, n]);
37
- }
38
- //#endregion
39
- //#region src/dom/useElementHeightObserver.ts
40
- function u(e) {
41
- let [t, r] = i(0);
42
- return n(() => {
43
- let t = e.current;
44
- if (!t) return;
45
- let n = new ResizeObserver((e) => {
46
- let n = e[0]?.contentRect.height ?? t.getBoundingClientRect().height;
47
- r(n);
48
- });
49
- return n.observe(t), () => n.disconnect();
50
- }, [e.current]), t;
51
- }
52
- //#endregion
53
- //#region src/dom/useEscapeDismiss.ts
54
- function d({ active: n, onDismiss: r, enabled: i = !0, documentTarget: a, containerRef: o }) {
55
- let s = t(() => r());
56
- e(() => {
57
- if (!n || !i) return;
58
- let e = a ?? (typeof document > "u" ? null : document);
59
- if (!e) return;
60
- let t = (t) => {
61
- if (t.key !== "Escape") return;
62
- let n = e.activeElement, r = o?.current;
63
- r && n instanceof Node && n !== r && !r.contains(n) || (t.preventDefault(), s());
64
- };
65
- return e.addEventListener("keydown", t, !0), () => e.removeEventListener("keydown", t, !0);
66
- }, [
67
- n,
68
- i,
69
- a,
70
- o
71
- ]);
72
- }
73
- //#endregion
74
- //#region src/dom/useOverlayFocus.ts
75
- var f = "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]):not([disabled])";
76
- function p({ active: n, trapFocus: i = !1, restoreFocus: a = !0, initialFocus: o = "auto", restoreFocusTarget: s, containerRef: c }) {
77
- let l = r(null), u = c ?? l, d = r(null), p = t((e) => Array.from(e.querySelectorAll(f)).filter((e) => !e.hasAttribute("aria-hidden"))), m = t((e) => {
78
- if (o === "container") return e;
79
- if (typeof o == "function") return o(e) ?? e;
80
- let t = p(e);
81
- return t.find((e) => e.hasAttribute("autofocus") || e.hasAttribute("autoFocus")) ?? t[0] ?? e;
82
- }), h = t(() => s?.() ?? d.current);
83
- return e(() => {
84
- if (!n) return;
85
- d.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
86
- let e = null, t = 0, r = () => {
87
- if (e = u.current, !e) {
88
- t = window.requestAnimationFrame(r);
89
- return;
90
- }
91
- let n = m(e);
92
- n instanceof HTMLElement && n.isConnected && n.focus({ preventScroll: !0 }), i && e.addEventListener("keydown", o);
93
- }, o = (t) => {
94
- if (!i || t.key !== "Tab" || !e) return;
95
- let n = p(e);
96
- if (n.length === 0) {
97
- t.preventDefault(), e.focus();
98
- return;
99
- }
100
- let r = n[0], a = n[n.length - 1], o = document.activeElement;
101
- if (t.shiftKey) {
102
- (o === r || o === e) && (t.preventDefault(), a.focus());
103
- return;
104
- }
105
- o === a && (t.preventDefault(), r.focus());
106
- };
107
- return r(), () => {
108
- if (window.cancelAnimationFrame(t), i && e && e.removeEventListener("keydown", o), !a) return;
109
- let n = h();
110
- n?.isConnected && n.focus({ preventScroll: !0 });
111
- };
112
- }, [
113
- n,
114
- i,
115
- a,
116
- u
117
- ]), u;
118
- }
119
- //#endregion
120
- //#region src/dom/useFocusTrap.ts
121
- var m = (e) => p({
122
- active: e,
123
- trapFocus: !0,
124
- restoreFocus: !0,
125
- initialFocus: "auto"
126
- });
127
- //#endregion
128
- //#region src/dom/useIntersectionObserver.ts
129
- function h(t, n) {
130
- let [r, a] = i(!1);
131
- return e(() => {
132
- let e = t.current;
133
- if (!e) return;
134
- let r = new IntersectionObserver(([e]) => {
135
- a(e.isIntersecting);
136
- }, n);
137
- return r.observe(e), () => {
138
- e && r.unobserve(e), r.disconnect();
139
- };
140
- }, [n, t]), { isIntersecting: r };
141
- }
142
- //#endregion
143
- //#region src/dom/useIsTouchDevice.ts
144
- var g = () => {
145
- let [t, n] = i(!1);
146
- return e(() => {
147
- let e = () => {
148
- let e = window.matchMedia("(pointer: coarse)").matches || "ontouchstart" in window || navigator.maxTouchPoints > 0;
149
- n(e);
150
- };
151
- return e(), window.addEventListener("resize", e), () => {
152
- window.removeEventListener("resize", e);
153
- };
154
- }, []), t;
155
- };
156
- //#endregion
157
- export { o as downloadFileFromBlob, s as downloadFileFromJson, a as downloadFileFromObjectURL, c as getOrCreatePortalRoot, l as useClickOutside, u as useElementHeightObserver, d as useEscapeDismiss, m as useFocusTrap, h as useIntersectionObserver, g as useIsTouchDevice, p as useOverlayFocus };
158
-
159
- //# sourceMappingURL=index.js.map
1
+ import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "../chunks/dom-RMvZy-Sx.js";
2
+ export { t as captureScrollAnchor, d as downloadFileFromBlob, n as downloadFileFromJson, r as downloadFileFromObjectURL, a as getOrCreatePortalRoot, l as useClickOutside, s as useElementHeightObserver, e as useEscapeDismiss, c as useFocusTrap, o as useIntersectionObserver, u as useIsTouchDevice, i as useOverlayFocus };
@@ -0,0 +1,8 @@
1
+ import { type CapturedScrollAnchor } from "./scrollAnchorTypes";
2
+ /**
3
+ * Захватывает позицию сохраняемого элемента перед изменением обычного вертикального списка.
4
+ * Вызывать только на browser event/effect boundary; restore — после DOM commit, release — при отмене.
5
+ * Новый захват того же viewport завершает предыдущий без прокрутки.
6
+ */
7
+ export declare function captureScrollAnchor(viewport: HTMLElement | null, anchor: HTMLElement | null): CapturedScrollAnchor | null;
8
+ //# sourceMappingURL=captureScrollAnchor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captureScrollAnchor.d.ts","sourceRoot":"","sources":["../../../src/dom/scroll-anchor/captureScrollAnchor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAKhE;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,oBAAoB,GAAG,IAAI,CA+CzH"}
@@ -0,0 +1,8 @@
1
+ /** Одноразовая DOM-операция; host обязан завершить её после commit либо отменить при ошибке/unmount. */
2
+ export type CapturedScrollAnchor = Readonly<{
3
+ /** true — прежний anchor ещё допустим и компенсация применена, в том числе при нулевом смещении. */
4
+ restore: () => boolean;
5
+ /** Освобождает временный стиль без изменения позиции прокрутки. Повторный вызов безопасен. */
6
+ release: () => void;
7
+ }>;
8
+ //# sourceMappingURL=scrollAnchorTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrollAnchorTypes.d.ts","sourceRoot":"","sources":["../../../src/dom/scroll-anchor/scrollAnchorTypes.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC3C,oGAAoG;IACpG,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,8FAA8F;IAC9F,OAAO,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC,CAAC"}
@@ -1,4 +1,4 @@
1
- import { downloadFileFromBlob as e } from "../dom/index.js";
1
+ import { u as e } from "../chunks/dom-RMvZy-Sx.js";
2
2
  import { Sn as t, c as n, l as r, pn as i, s as a, u as o, xn as s } from "../chunks/odata-service-CWkHASdw.js";
3
3
  import { d as c, t as l } from "../chunks/validate-DyzojM0w.js";
4
4
  import { escapeXmlValue as u } from "../xml/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuzaki13/react-foundation-lib",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Reusable React and TypeScript foundation utilities, hooks, and primitives.",
5
5
  "license": "MIT",
6
6
  "author": "Ryuzaki13",
@@ -13,6 +13,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
13
13
  - установить начальный фокус, зациклить `Tab` и восстановить прежний фокус;
14
14
  - узнать высоту элемента через `ResizeObserver`;
15
15
  - узнать, пересекается ли элемент с viewport или другим root;
16
+ - сохранить позицию чтения при вставке элементов выше текущего;
16
17
  - определить наличие touch/coarse-pointer возможностей.
17
18
 
18
19
  Модуль является низкоуровневым browser foundation-слоем. Он не создаёт готовые кнопки, dialog, modal, popover, tooltip или download UI. Визуальный компонент по-прежнему отвечает за семантический HTML, ARIA, стили, анимацию, стек overlay, сообщения об ошибках и пользовательский сценарий.
@@ -27,6 +28,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
27
28
  - [Быстрый старт](#быстрый-старт)
28
29
  - [Скачивание файлов](#скачивание-файлов)
29
30
  - [`getOrCreatePortalRoot`](#getorcreateportalroot)
31
+ - [`captureScrollAnchor`](#capturescrollanchor)
30
32
  - [`useClickOutside`](#useclickoutside)
31
33
  - [`useEscapeDismiss`](#useescapedismiss)
32
34
  - [`useElementHeightObserver`](#useelementheightobserver)
@@ -103,6 +105,7 @@ npm install react react-dom
103
105
 
104
106
  ```ts
105
107
  import {
108
+ captureScrollAnchor,
106
109
  downloadFileFromBlob,
107
110
  downloadFileFromJson,
108
111
  downloadFileFromObjectURL,
@@ -272,6 +275,7 @@ Focus определяет, какой элемент получает keyboard
272
275
  | Скачать готовый `Blob` | `downloadFileFromBlob` |
273
276
  | Сериализовать значение в readable JSON и скачать | `downloadFileFromJson` |
274
277
  | Получить общий container для `createPortal` | `getOrCreatePortalRoot` |
278
+ | Сохранить позицию при вставке элементов сверху | `captureScrollAnchor` |
275
279
  | Закрыть UI по `mousedown` вне одного элемента | `useClickOutside` |
276
280
  | Считать trigger и portal content одной «внутренней» областью | `useClickOutside` с массивом refs |
277
281
  | Закрыть активный overlay по `Escape` | `useEscapeDismiss` |
@@ -668,6 +672,81 @@ getOrCreatePortalRoot("dialog-root"); // null, если document отсутст
668
672
 
669
673
  Не кешируйте этот `null` в server-loaded module и не ожидайте, что он автоматически станет HTMLElement на client. Portal target должен разрешаться на корректной browser lifecycle boundary.
670
674
 
675
+ ## `captureScrollAnchor`
676
+
677
+ Одноразовая компенсация сдвига сохраняемого DOM-элемента в обычном
678
+ вертикальном scroll-контейнере. Например, при добавлении более ранних
679
+ элементов в начало списка. Не запускает запросы и не зависит от React,
680
+ Query, cursor или предметной модели.
681
+
682
+ ```ts
683
+ declare function captureScrollAnchor(viewport: HTMLElement | null, anchor: HTMLElement | null): CapturedScrollAnchor | null;
684
+
685
+ type CapturedScrollAnchor = Readonly<{
686
+ restore: () => boolean;
687
+ release: () => void;
688
+ }>;
689
+ ```
690
+
691
+ Host выбирает сохраняемый элемент, захватывает его **до изменения DOM**,
692
+ вызывает `restore()` **после DOM commit**, а при отмене, ошибке или unmount —
693
+ `release()`. Разрешение Promise загрузки не гарантирует DOM commit: React-host
694
+ применяет восстановление в layout effect соответствующего обновления, а не
695
+ сразу после `await fetchNextPage()`.
696
+
697
+ ```ts
698
+ const pendingAnchor = captureScrollAnchor(viewportElement, retainedElement);
699
+ try {
700
+ // Здесь пример синхронного DOM commit, не React render.
701
+ listElement.prepend(olderElementsFragment);
702
+ pendingAnchor?.restore();
703
+ } finally {
704
+ pendingAnchor?.release();
705
+ }
706
+ ```
707
+
708
+ Операция сохраняет положение anchor в координатах содержимого контейнера.
709
+ Разницу после commit прибавляет к **текущему** `scrollTop`: ручная прокрутка
710
+ во время ожидания не отменяется. Компенсация мгновенная даже при CSS
711
+ `scroll-behavior: smooth`; горизонтальная позиция не меняется. При нулевом
712
+ сдвиге запись прокрутки не выполняется. Допустимый диапазон ограничивает
713
+ сам браузер.
714
+
715
+ На время операции `overflow-anchor` контейнера устанавливается в
716
+ `none!important`, чтобы браузер не выполнил вторую компенсацию. При завершении
717
+ возвращаются исходные inline-значение и priority либо удаляется отсутствовавшее
718
+ свойство. Внешнее изменение этого свойства отменяет компенсацию и не
719
+ перезаписывается: native anchoring уже мог возобновиться. Браузер без
720
+ поддержки `overflow-anchor` не требует отключения native anchoring.
721
+
722
+ На контейнер допускается один активный захват: новый корректный вызов
723
+ отменяет предыдущий без прокрутки. Его старый handle становится неактивным
724
+ и не может освободить стиль нового захвата. Разные контейнеры независимы.
725
+
726
+ `null` возвращается для отсутствующих или отключённых от документа элементов,
727
+ самого viewport в качестве anchor и элемента вне viewport. `restore()`
728
+ возвращает `true`, если anchor сохранился и операция выполнена, включая нулевой
729
+ сдвиг; `false` — если операция уже завершена либо DOM больше не подходит.
730
+ `release()` и повторный `restore()` безопасны. Временный стиль освобождается
731
+ и при ошибке browser API; ошибка не подавляется.
732
+ После завершения host отбрасывает handle: пока он хранится, его closure
733
+ содержит ссылки на захваченные DOM-элементы.
734
+
735
+ Границы: anchor должен сохранять DOM identity и обычное положение в потоке
736
+ без промежуточного независимо прокручиваемого контейнера. Не использовать
737
+ для window-scroll, горизонтальной или reverse-flex ленты, sticky/fixed anchor,
738
+ CSS scroll-snap и преобразования масштаба/поворота. Изменения layout после
739
+ завершения операции, например поздние изображения, не отслеживаются. API не
740
+ создаёт listeners, observers, timers или React state. Его импорт SSR-safe;
741
+ во время render вызов с реальными DOM-элементами запрещён.
742
+
743
+ Реальная геометрия, native anchoring, CSS smooth и browser clamp проверяются
744
+ в [browser fixture](./scroll-anchor/scroll-anchor.browser.html). Для повторения
745
+ запустить `npx vite dev --host 127.0.0.1 --port 6011 --strictPort`, открыть
746
+ `http://127.0.0.1:6011/src/dom/scroll-anchor/scroll-anchor.browser.html` и нажать
747
+ «Запустить проверки». Результаты и измерения выводятся на странице; этот
748
+ dev-only сценарий дополняет unit-тесты, а не заменяет проверку React-host.
749
+
671
750
  ## `useClickOutside`
672
751
 
673
752
  Вызывает handler на `mousedown`, если target находится вне одного ref или вне всех refs из массива.
@@ -2055,6 +2134,7 @@ Touch-capable laptop не обязательно mobile. Не связывайт
2055
2134
  | `downloadFileFromBlob` | filename, `Blob` | `void` | Сам создаёт/revokes URL | Browser-only, ошибки синхронные. |
2056
2135
  | `downloadFileFromJson` | filename, `unknown` | `void` | JSON с 2 spaces, UTF-8 MIME | Наследует ошибки и потери `JSON.stringify`. |
2057
2136
  | `getOrCreatePortalRoot` | `id: string` | `HTMLElement \| null` | Создаёт `div` в body | DOM mutation; root не удаляется. |
2137
+ | `captureScrollAnchor` | viewport, сохраняемый элемент | `CapturedScrollAnchor \| null` | Один активный захват на viewport | Обычный вертикальный DOM flow; restore после commit. |
2058
2138
  | `useClickOutside` | один ref/массив refs, handler | `void` | Global bubbling `mousedown` | Нет enabled/event arg/custom document. |
2059
2139
  | `useEscapeDismiss` | options object | `void` | enabled=true, capture `keydown` | Не управляет stack/focus; defaultPrevented игнорируется. |
2060
2140
  | `useElementHeightObserver` | element ref | `number` | Initial `0` | ContentRect, требует ResizeObserver. |
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/dom/downloadFile.ts","../../src/dom/getOrCreatePortalRoot.ts","../../src/dom/useClickOutside.ts","../../src/dom/useElementHeightObserver.ts","../../src/dom/useEscapeDismiss.ts","../../src/dom/useOverlayFocus.ts","../../src/dom/useFocusTrap.ts","../../src/dom/useIntersectionObserver.ts","../../src/dom/useIsTouchDevice.ts"],"sourcesContent":["export function downloadFileFromObjectURL(filename: string, objectUrl: string) {\n\tconst link = document.createElement(\"a\");\n\n\tlink.href = objectUrl;\n\tlink.download = filename;\n\tlink.click();\n\n\twindow.setTimeout(() => {\n\t\twindow.URL.revokeObjectURL(objectUrl);\n\t}, 0);\n}\n\nexport function downloadFileFromBlob(filename: string, blob: Blob) {\n\tconst objectUrl = window.URL.createObjectURL(blob);\n\tdownloadFileFromObjectURL(filename, objectUrl);\n}\n\nexport function downloadFileFromJson(filename: string, payload: unknown) {\n\tconst blob = new Blob([JSON.stringify(payload, null, 2)], { type: \"application/json;charset=utf-8\" });\n\tdownloadFileFromBlob(filename, blob);\n}\n","export function getOrCreatePortalRoot(id: string): HTMLElement | null {\n\tif (typeof document === \"undefined\") {\n\t\treturn null;\n\t}\n\n\tconst existingElement = document.getElementById(id);\n\tif (existingElement instanceof HTMLElement) {\n\t\treturn existingElement;\n\t}\n\n\tconst portalRoot = document.createElement(\"div\");\n\tportalRoot.id = id;\n\tdocument.body.append(portalRoot);\n\n\treturn portalRoot;\n}\n","import { RefObject, useEffect } from \"react\";\n\nexport function useClickOutside(ref: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[], handler: () => void) {\n\tuseEffect(() => {\n\t\tconst handleClickOutside = (event: MouseEvent) => {\n\t\t\tif (Array.isArray(ref)) {\n\t\t\t\tlet isOutside = true;\n\t\t\t\tfor (const r of ref) {\n\t\t\t\t\tif (r.current && r.current.contains(event.target as Node)) {\n\t\t\t\t\t\tisOutside = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (isOutside) {\n\t\t\t\t\thandler();\n\t\t\t\t}\n\t\t\t} else if (ref.current && !ref.current.contains(event.target as Node)) {\n\t\t\t\thandler();\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener(\"mousedown\", handleClickOutside);\n\t\treturn () => document.removeEventListener(\"mousedown\", handleClickOutside);\n\t}, [ref, handler]);\n}\n","import { type RefObject, useLayoutEffect, useState } from \"react\";\n\nexport function useElementHeightObserver<TElement extends HTMLElement>(elementRef: RefObject<TElement | null>) {\n\tconst [height, setHeight] = useState(0);\n\n\tuseLayoutEffect(() => {\n\t\tconst element = elementRef.current;\n\t\tif (!element) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst nextHeight = entries[0]?.contentRect.height ?? element.getBoundingClientRect().height;\n\t\t\tsetHeight(nextHeight);\n\t\t});\n\t\tobserver.observe(element);\n\n\t\treturn () => observer.disconnect();\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/refs\n\t}, [elementRef.current]);\n\n\treturn height;\n}\n","import { RefObject, useEffect, useEffectEvent } from \"react\";\n\ninterface UseEscapeDismissOptions {\n\tactive: boolean;\n\tonDismiss: () => void;\n\tenabled?: boolean;\n\tdocumentTarget?: Document | null;\n\tcontainerRef?: RefObject<HTMLElement | null>;\n}\n\n/**\n * Подписывает overlay на закрытие по клавише Escape.\n */\nexport function useEscapeDismiss({ active, onDismiss, enabled = true, documentTarget, containerRef }: UseEscapeDismissOptions) {\n\tconst dismiss = useEffectEvent(() => onDismiss());\n\n\tuseEffect(() => {\n\t\tif (!active || !enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst targetDocument = documentTarget ?? (typeof document === \"undefined\" ? null : document);\n\t\tif (!targetDocument) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst handleKeyDown = (event: KeyboardEvent) => {\n\t\t\tif (event.key !== \"Escape\" /*|| event.defaultPrevented*/) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst activeElement = targetDocument.activeElement;\n\t\t\tconst container = containerRef?.current;\n\t\t\tif (container && activeElement instanceof Node && activeElement !== container && !container.contains(activeElement)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tevent.preventDefault();\n\t\t\tdismiss();\n\t\t};\n\n\t\ttargetDocument.addEventListener(\"keydown\", handleKeyDown, true);\n\t\treturn () => targetDocument.removeEventListener(\"keydown\", handleKeyDown, true);\n\t}, [active, enabled, documentTarget, containerRef]);\n}\n","import { RefObject, useEffect, useEffectEvent, useRef } from \"react\";\n\nconst focusableElementsSelector =\n\t'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]):not([disabled])';\n\nexport interface UseOverlayFocusOptions<T extends HTMLElement> {\n\tactive: boolean;\n\ttrapFocus?: boolean;\n\trestoreFocus?: boolean;\n\tinitialFocus?: \"auto\" | \"container\" | ((container: T) => HTMLElement | null | undefined);\n\trestoreFocusTarget?: () => HTMLElement | null | undefined;\n\tcontainerRef?: RefObject<T | null>;\n}\n\n/**\n * Возвращает ref overlay-контейнера и управляет начальными/возвратными переходами фокуса.\n * Открытие и закрытие не прокручивают документ: портал может получить фокус\n * до завершения позиционирования. Пользовательский Tab по-прежнему раскрывает\n * выбранный элемент в прокручиваемом содержимом overlay.\n */\nexport function useOverlayFocus<T extends HTMLElement>({\n\tactive,\n\ttrapFocus = false,\n\trestoreFocus = true,\n\tinitialFocus = \"auto\",\n\trestoreFocusTarget,\n\tcontainerRef\n}: UseOverlayFocusOptions<T>) {\n\tconst internalOverlayRef = useRef<T | null>(null);\n\tconst overlayRef = containerRef ?? internalOverlayRef;\n\tconst previousActiveElementRef = useRef<HTMLElement | null>(null);\n\n\t/**\n\t * Собирает фокусируемые элементы overlay в актуальном DOM-порядке.\n\t */\n\tconst getFocusableElements = useEffectEvent((container: T) =>\n\t\tArray.from(container.querySelectorAll<HTMLElement>(focusableElementsSelector)).filter(\n\t\t\t(element) => !element.hasAttribute(\"aria-hidden\")\n\t\t)\n\t);\n\n\t/**\n\t * Вычисляет стартовый фокус при открытии overlay.\n\t */\n\tconst resolveInitialFocus = useEffectEvent((container: T) => {\n\t\tif (initialFocus === \"container\") {\n\t\t\treturn container;\n\t\t}\n\n\t\tif (typeof initialFocus === \"function\") {\n\t\t\treturn initialFocus(container) ?? container;\n\t\t}\n\n\t\tconst focusableElements = getFocusableElements(container);\n\t\treturn (\n\t\t\tfocusableElements.find((element) => element.hasAttribute(\"autofocus\") || element.hasAttribute(\"autoFocus\")) ??\n\t\t\tfocusableElements[0] ??\n\t\t\tcontainer\n\t\t);\n\t});\n\n\t/**\n\t * Вычисляет элемент, которому нужно вернуть фокус после закрытия overlay.\n\t */\n\tconst resolveRestoreFocusTarget = useEffectEvent(() => restoreFocusTarget?.() ?? previousActiveElementRef.current);\n\n\tuseEffect(() => {\n\t\tif (!active) {\n\t\t\treturn;\n\t\t}\n\n\t\tpreviousActiveElementRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n\t\tlet overlayElement: T | null = null;\n\t\tlet rafId = 0;\n\n\t\t/**\n\t\t * Цикл откладывает инициализацию, пока ref контейнера не будет привязан к DOM.\n\t\t */\n\t\tconst attachOverlay = () => {\n\t\t\toverlayElement = overlayRef.current;\n\t\t\tif (!overlayElement) {\n\t\t\t\trafId = window.requestAnimationFrame(attachOverlay);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst focusTarget = resolveInitialFocus(overlayElement);\n\t\t\tif (focusTarget instanceof HTMLElement && focusTarget.isConnected) {\n\t\t\t\tfocusTarget.focus({ preventScroll: true });\n\t\t\t}\n\n\t\t\tif (trapFocus) {\n\t\t\t\toverlayElement.addEventListener(\"keydown\", handleTabKey);\n\t\t\t}\n\t\t};\n\n\t\t/**\n\t\t * Зацикливает Tab внутри overlay, пока он открыт как модальный слой.\n\t\t */\n\t\tconst handleTabKey = (event: KeyboardEvent) => {\n\t\t\tif (!trapFocus || event.key !== \"Tab\" || !overlayElement) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst focusableElements = getFocusableElements(overlayElement);\n\t\t\tif (focusableElements.length === 0) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\toverlayElement.focus();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst firstElement = focusableElements[0];\n\t\t\tconst lastElement = focusableElements[focusableElements.length - 1];\n\t\t\tconst activeElement = document.activeElement;\n\n\t\t\tif (event.shiftKey) {\n\t\t\t\tif (activeElement === firstElement || activeElement === overlayElement) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tlastElement.focus();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (activeElement === lastElement) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tfirstElement.focus();\n\t\t\t}\n\t\t};\n\n\t\tattachOverlay();\n\n\t\treturn () => {\n\t\t\twindow.cancelAnimationFrame(rafId);\n\t\t\tif (trapFocus && overlayElement) {\n\t\t\t\toverlayElement.removeEventListener(\"keydown\", handleTabKey);\n\t\t\t}\n\n\t\t\tif (!restoreFocus) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst restoreTarget = resolveRestoreFocusTarget();\n\t\t\tif (restoreTarget?.isConnected) {\n\t\t\t\trestoreTarget.focus({ preventScroll: true });\n\t\t\t}\n\t\t};\n\t}, [active, trapFocus, restoreFocus, overlayRef]);\n\n\treturn overlayRef;\n}\n","import { useOverlayFocus } from \"./useOverlayFocus\";\n\n/**\n * Совместимый wrapper для overlay с фокус-ловушкой.\n */\nexport const useFocusTrap = (isOpen: boolean) => {\n\treturn useOverlayFocus<HTMLDivElement>({ active: isOpen, trapFocus: true, restoreFocus: true, initialFocus: \"auto\" });\n};\n","import { useEffect, useState } from \"react\";\n\nexport function useIntersectionObserver(ref: React.RefObject<Element | null>, options?: IntersectionObserverInit) {\n\tconst [isIntersecting, setIsIntersecting] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst el = ref.current;\n\t\tif (!el) return;\n\n\t\tconst observer = new IntersectionObserver(([entry]) => {\n\t\t\tsetIsIntersecting(entry.isIntersecting);\n\t\t}, options);\n\n\t\tobserver.observe(el);\n\n\t\treturn () => {\n\t\t\tif (el) {\n\t\t\t\tobserver.unobserve(el);\n\t\t\t}\n\t\t\tobserver.disconnect();\n\t\t};\n\t}, [options, ref]); // не забыть, что ref.current может меняться\n\n\treturn { isIntersecting };\n}\n","import { useEffect, useState } from \"react\";\n\nexport const useIsTouchDevice = () => {\n\tconst [isTouchDevice, setIsTouchDevice] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst checkTouchDevice = () => {\n\t\t\tconst isTouch = window.matchMedia(\"(pointer: coarse)\").matches || \"ontouchstart\" in window || navigator.maxTouchPoints > 0;\n\t\t\tsetIsTouchDevice(isTouch);\n\t\t};\n\n\t\tcheckTouchDevice();\n\n\t\t// Обработчик для изменений в типе устройства\n\t\twindow.addEventListener(\"resize\", checkTouchDevice);\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"resize\", checkTouchDevice);\n\t\t};\n\t}, []);\n\n\treturn isTouchDevice;\n};\n"],"mappings":"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"}