@jbpark/use-hooks 2.9.0 → 2.10.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.
- package/dist/hooks/use-body-scroll-lock.mjs +19 -51
- package/dist/hooks/use-body-scroll-lock.mjs.map +1 -1
- package/dist/hooks/use-history-state.mjs +5 -3
- package/dist/hooks/use-history-state.mjs.map +1 -1
- package/dist/hooks/use-scroll-to-elements.mjs +16 -10
- package/dist/hooks/use-scroll-to-elements.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-body-scroll-lock.ts
|
|
4
|
-
|
|
4
|
+
const activeLocks = /* @__PURE__ */ new Set();
|
|
5
5
|
let originalStyles = null;
|
|
6
6
|
let originalScrollY = 0;
|
|
7
|
-
const isIOS = () => /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
8
|
-
const lockScroll = () => {
|
|
9
|
-
if (
|
|
7
|
+
const isIOS = () => typeof navigator !== "undefined" && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
8
|
+
const lockScroll = (token) => {
|
|
9
|
+
if (activeLocks.size === 0) {
|
|
10
10
|
originalScrollY = window.scrollY;
|
|
11
11
|
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
12
12
|
const currentPaddingRight = parseFloat(getComputedStyle(document.body).paddingRight) || 0;
|
|
@@ -43,11 +43,11 @@ const lockScroll = () => {
|
|
|
43
43
|
if (scrollbarWidth > 0) document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;
|
|
44
44
|
if (isIOS()) document.body.style.setProperty("-webkit-overflow-scrolling", "touch");
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
activeLocks.add(token);
|
|
47
47
|
};
|
|
48
|
-
const unlockScroll = () => {
|
|
49
|
-
|
|
50
|
-
if (
|
|
48
|
+
const unlockScroll = (token) => {
|
|
49
|
+
activeLocks.delete(token);
|
|
50
|
+
if (activeLocks.size > 0 || !originalStyles) return;
|
|
51
51
|
document.documentElement.style.overflow = originalStyles.documentElement.overflow;
|
|
52
52
|
document.documentElement.style.height = originalStyles.documentElement.height;
|
|
53
53
|
document.documentElement.style.position = originalStyles.documentElement.position;
|
|
@@ -67,54 +67,22 @@ const unlockScroll = () => {
|
|
|
67
67
|
};
|
|
68
68
|
const useBodyScrollLock = (enabled = true) => {
|
|
69
69
|
useEffect(() => {
|
|
70
|
-
if (!enabled) return;
|
|
71
|
-
|
|
70
|
+
if (typeof window === "undefined" || !enabled) return;
|
|
71
|
+
const token = Symbol("body-scroll-lock");
|
|
72
|
+
lockScroll(token);
|
|
73
|
+
let removeTouchListener;
|
|
72
74
|
if (isIOS()) {
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
e.preventDefault();
|
|
76
|
-
e.stopPropagation();
|
|
77
|
-
e.stopImmediatePropagation();
|
|
78
|
-
}
|
|
75
|
+
const preventTouchMove = (event) => {
|
|
76
|
+
if (event.target === document.body || event.target === document.documentElement) event.preventDefault();
|
|
79
77
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
document.
|
|
83
|
-
document.documentElement.scrollTop = 0;
|
|
84
|
-
};
|
|
85
|
-
const events = [
|
|
86
|
-
"scroll",
|
|
87
|
-
"touchmove",
|
|
88
|
-
"touchstart",
|
|
89
|
-
"touchend"
|
|
90
|
-
];
|
|
91
|
-
events.forEach((event) => {
|
|
92
|
-
window.addEventListener(event, preventAll, {
|
|
93
|
-
passive: false,
|
|
94
|
-
capture: true
|
|
95
|
-
});
|
|
96
|
-
document.addEventListener(event, preventAll, {
|
|
97
|
-
passive: false,
|
|
98
|
-
capture: true
|
|
99
|
-
});
|
|
100
|
-
document.body.addEventListener(event, preventAll, {
|
|
101
|
-
passive: false,
|
|
102
|
-
capture: true
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
const scrollInterval = setInterval(preventScroll, 16);
|
|
106
|
-
return () => {
|
|
107
|
-
clearInterval(scrollInterval);
|
|
108
|
-
events.forEach((event) => {
|
|
109
|
-
window.removeEventListener(event, preventAll, { capture: true });
|
|
110
|
-
document.removeEventListener(event, preventAll, { capture: true });
|
|
111
|
-
document.body.removeEventListener(event, preventAll, { capture: true });
|
|
112
|
-
});
|
|
113
|
-
unlockScroll();
|
|
78
|
+
document.addEventListener("touchmove", preventTouchMove, { passive: false });
|
|
79
|
+
removeTouchListener = () => {
|
|
80
|
+
document.removeEventListener("touchmove", preventTouchMove);
|
|
114
81
|
};
|
|
115
82
|
}
|
|
116
83
|
return () => {
|
|
117
|
-
|
|
84
|
+
removeTouchListener?.();
|
|
85
|
+
unlockScroll(token);
|
|
118
86
|
};
|
|
119
87
|
}, [enabled]);
|
|
120
88
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-body-scroll-lock.mjs","names":[],"sources":["../../src/hooks/use-body-scroll-lock.ts"],"sourcesContent":["import { useEffect } from 'react';\n\ninterface LockedStyles {\n documentElement: {\n overflow: string;\n height: string;\n position: string;\n width: string;\n };\n body: {\n overflow: string;\n height: string;\n position: string;\n top: string;\n left: string;\n right: string;\n width: string;\n paddingRight: string;\n webkitOverflowScrolling: string;\n };\n}\n\
|
|
1
|
+
{"version":3,"file":"use-body-scroll-lock.mjs","names":[],"sources":["../../src/hooks/use-body-scroll-lock.ts"],"sourcesContent":["import { useEffect } from 'react';\n\ninterface LockedStyles {\n documentElement: {\n overflow: string;\n height: string;\n position: string;\n width: string;\n };\n body: {\n overflow: string;\n height: string;\n position: string;\n top: string;\n left: string;\n right: string;\n width: string;\n paddingRight: string;\n webkitOverflowScrolling: string;\n };\n}\n\n// A Set of per-instance tokens instead of a raw counter — deleting the same\n// token twice (or locking with a token that's already in the set) can't\n// drift the count the way increment/decrement pairs could.\nconst activeLocks = new Set<symbol>();\nlet originalStyles: LockedStyles | null = null;\nlet originalScrollY = 0;\n\nconst isIOS = () =>\n typeof navigator !== 'undefined' &&\n /iPad|iPhone|iPod/.test(navigator.userAgent);\n\nconst lockScroll = (token: symbol) => {\n if (activeLocks.size === 0) {\n originalScrollY = window.scrollY;\n const scrollbarWidth =\n window.innerWidth - document.documentElement.clientWidth;\n const currentPaddingRight =\n parseFloat(getComputedStyle(document.body).paddingRight) || 0;\n\n originalStyles = {\n documentElement: {\n overflow: document.documentElement.style.overflow,\n height: document.documentElement.style.height,\n position: document.documentElement.style.position,\n width: document.documentElement.style.width,\n },\n body: {\n overflow: document.body.style.overflow,\n height: document.body.style.height,\n position: document.body.style.position,\n top: document.body.style.top,\n left: document.body.style.left,\n right: document.body.style.right,\n width: document.body.style.width,\n paddingRight: document.body.style.paddingRight,\n webkitOverflowScrolling: document.body.style.getPropertyValue(\n '-webkit-overflow-scrolling',\n ),\n },\n };\n\n document.documentElement.style.overflow = 'hidden';\n document.documentElement.style.height = '100%';\n document.documentElement.style.position = 'fixed';\n document.documentElement.style.width = '100%';\n\n document.body.style.overflow = 'hidden';\n document.body.style.height = '100%';\n document.body.style.position = 'fixed';\n document.body.style.top = `-${originalScrollY}px`;\n document.body.style.left = '0';\n document.body.style.right = '0';\n document.body.style.width = '100%';\n\n if (scrollbarWidth > 0) {\n document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;\n }\n\n if (isIOS()) {\n document.body.style.setProperty('-webkit-overflow-scrolling', 'touch');\n }\n }\n\n activeLocks.add(token);\n};\n\nconst unlockScroll = (token: symbol) => {\n activeLocks.delete(token);\n\n if (activeLocks.size > 0 || !originalStyles) {\n return;\n }\n\n document.documentElement.style.overflow =\n originalStyles.documentElement.overflow;\n document.documentElement.style.height = originalStyles.documentElement.height;\n document.documentElement.style.position =\n originalStyles.documentElement.position;\n document.documentElement.style.width = originalStyles.documentElement.width;\n\n document.body.style.overflow = originalStyles.body.overflow;\n document.body.style.height = originalStyles.body.height;\n document.body.style.position = originalStyles.body.position;\n document.body.style.top = originalStyles.body.top;\n document.body.style.left = originalStyles.body.left;\n document.body.style.right = originalStyles.body.right;\n document.body.style.width = originalStyles.body.width;\n document.body.style.paddingRight = originalStyles.body.paddingRight;\n\n if (originalStyles.body.webkitOverflowScrolling) {\n document.body.style.setProperty(\n '-webkit-overflow-scrolling',\n originalStyles.body.webkitOverflowScrolling,\n );\n } else {\n document.body.style.removeProperty('-webkit-overflow-scrolling');\n }\n\n window.scrollTo(0, originalScrollY);\n originalStyles = null;\n};\n\nconst useBodyScrollLock = (enabled: boolean = true) => {\n useEffect(() => {\n if (typeof window === 'undefined' || !enabled) {\n return;\n }\n\n const token = Symbol('body-scroll-lock');\n lockScroll(token);\n\n // The position:fixed styles above are the real lock — this is only a\n // narrow safety net for iOS Safari, which can still rubber-band the\n // page on a touchmove that starts directly on the body/html background.\n // Only that one event is handled (no capture, no stopPropagation), so\n // a scrollable element inside the locked content (e.g. a modal body)\n // keeps scrolling normally — its touchmove target isn't body/html.\n let removeTouchListener: (() => void) | undefined;\n\n if (isIOS()) {\n const preventTouchMove = (event: TouchEvent) => {\n if (\n event.target === document.body ||\n event.target === document.documentElement\n ) {\n event.preventDefault();\n }\n };\n\n document.addEventListener('touchmove', preventTouchMove, {\n passive: false,\n });\n\n removeTouchListener = () => {\n document.removeEventListener('touchmove', preventTouchMove);\n };\n }\n\n return () => {\n removeTouchListener?.();\n unlockScroll(token);\n };\n }, [enabled]);\n};\n\nexport default useBodyScrollLock;\n"],"mappings":"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"}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { useCallback, useReducer } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-history-state.ts
|
|
4
|
+
const takeLast = (arr, limit) => limit <= 0 ? [] : arr.slice(-limit);
|
|
5
|
+
const takeFirst = (arr, limit) => limit <= 0 ? [] : arr.slice(0, limit);
|
|
4
6
|
const createReducer = (limit) => (state, action) => {
|
|
5
7
|
switch (action.type) {
|
|
6
8
|
case "SET": {
|
|
7
9
|
const resolved = action.value instanceof Function ? action.value(state.present) : action.value;
|
|
8
10
|
if (resolved === state.present) return state;
|
|
9
11
|
return {
|
|
10
|
-
past: [...state.past, state.present]
|
|
12
|
+
past: takeLast([...state.past, state.present], limit),
|
|
11
13
|
present: resolved,
|
|
12
14
|
future: []
|
|
13
15
|
};
|
|
@@ -18,14 +20,14 @@ const createReducer = (limit) => (state, action) => {
|
|
|
18
20
|
return {
|
|
19
21
|
past: state.past.slice(0, -1),
|
|
20
22
|
present: previous,
|
|
21
|
-
future: [state.present, ...state.future]
|
|
23
|
+
future: takeFirst([state.present, ...state.future], limit)
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
case "REDO": {
|
|
25
27
|
if (state.future.length === 0) return state;
|
|
26
28
|
const [next, ...rest] = state.future;
|
|
27
29
|
return {
|
|
28
|
-
past: [...state.past, state.present]
|
|
30
|
+
past: takeLast([...state.past, state.present], limit),
|
|
29
31
|
present: next,
|
|
30
32
|
future: rest
|
|
31
33
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-history-state.mjs","names":[],"sources":["../../src/hooks/use-history-state.ts"],"sourcesContent":["import { useCallback, useReducer } from 'react';\n\ninterface HistoryState<T> {\n past: T[];\n present: T;\n future: T[];\n}\n\ntype Action<T> =\n | { type: 'SET'; value: T | ((prev: T) => T) }\n | { type: 'UNDO' }\n | { type: 'REDO' }\n | { type: 'RESET'; value: T };\n\nconst createReducer =\n <T>(limit: number) =>\n (state: HistoryState<T>, action: Action<T>): HistoryState<T> => {\n switch (action.type) {\n case 'SET': {\n const resolved =\n action.value instanceof Function\n ? action.value(state.present)\n : action.value;\n\n if (resolved === state.present) {\n return state;\n }\n\n return {\n past: [...state.past, state.present]
|
|
1
|
+
{"version":3,"file":"use-history-state.mjs","names":[],"sources":["../../src/hooks/use-history-state.ts"],"sourcesContent":["import { useCallback, useReducer } from 'react';\n\ninterface HistoryState<T> {\n past: T[];\n present: T;\n future: T[];\n}\n\n// `arr.slice(-limit)` breaks for `limit <= 0`: `slice(-0)` is `slice(0)`,\n// which keeps the whole array instead of truncating to nothing. Treating\n// any non-positive limit as \"keep none\" makes `limit: 0` (and negative\n// limits, previously undefined behavior) actually mean no history.\nconst takeLast = <T>(arr: T[], limit: number): T[] =>\n limit <= 0 ? [] : arr.slice(-limit);\n\nconst takeFirst = <T>(arr: T[], limit: number): T[] =>\n limit <= 0 ? [] : arr.slice(0, limit);\n\ntype Action<T> =\n | { type: 'SET'; value: T | ((prev: T) => T) }\n | { type: 'UNDO' }\n | { type: 'REDO' }\n | { type: 'RESET'; value: T };\n\nconst createReducer =\n <T>(limit: number) =>\n (state: HistoryState<T>, action: Action<T>): HistoryState<T> => {\n switch (action.type) {\n case 'SET': {\n const resolved =\n action.value instanceof Function\n ? action.value(state.present)\n : action.value;\n\n if (resolved === state.present) {\n return state;\n }\n\n return {\n past: takeLast([...state.past, state.present], limit),\n present: resolved,\n future: [],\n };\n }\n\n case 'UNDO': {\n if (state.past.length === 0) {\n return state;\n }\n\n const previous = state.past[state.past.length - 1] as T;\n\n return {\n past: state.past.slice(0, -1),\n present: previous,\n // Cap `future` at `limit` too — otherwise a long run of undos\n // (more than `limit` of them) grows `future` unbounded, even\n // though `past` is kept within `limit` by REDO/SET.\n future: takeFirst([state.present, ...state.future], limit),\n };\n }\n\n case 'REDO': {\n if (state.future.length === 0) {\n return state;\n }\n\n const [next, ...rest] = state.future;\n\n return {\n past: takeLast([...state.past, state.present], limit),\n present: next as T,\n future: rest,\n };\n }\n\n case 'RESET': {\n return { past: [], present: action.value, future: [] };\n }\n\n default: {\n return state;\n }\n }\n };\n\ninterface UseHistoryStateOptions {\n limit?: number;\n}\n\nconst DEFAULT_LIMIT = 50;\n\nconst useHistoryState = <T>(\n initialValue: T,\n options?: UseHistoryStateOptions,\n) => {\n const limit = options?.limit ?? DEFAULT_LIMIT;\n\n const [state, dispatch] = useReducer(createReducer<T>(limit), {\n past: [],\n present: initialValue,\n future: [],\n });\n\n const setValue = useCallback((value: T | ((prev: T) => T)) => {\n dispatch({ type: 'SET', value });\n }, []);\n\n const undo = useCallback(() => {\n dispatch({ type: 'UNDO' });\n }, []);\n\n const redo = useCallback(() => {\n dispatch({ type: 'REDO' });\n }, []);\n\n const reset = useCallback((value: T) => {\n dispatch({ type: 'RESET', value });\n }, []);\n\n return {\n value: state.present,\n setValue,\n undo,\n redo,\n reset,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n } as const;\n};\n\nexport default useHistoryState;\n"],"mappings":";;;AAYA,MAAM,YAAe,KAAU,UAC7B,SAAS,IAAI,EAAE,GAAG,IAAI,MAAM,CAAC,MAAM;AAErC,MAAM,aAAgB,KAAU,UAC9B,SAAS,IAAI,EAAE,GAAG,IAAI,MAAM,GAAG,MAAM;AAQvC,MAAM,iBACA,WACH,OAAwB,WAAuC;AAC9D,SAAQ,OAAO,MAAf;EACE,KAAK,OAAO;GACV,MAAM,WACJ,OAAO,iBAAiB,WACpB,OAAO,MAAM,MAAM,QAAQ,GAC3B,OAAO;AAEb,OAAI,aAAa,MAAM,QACrB,QAAO;AAGT,UAAO;IACL,MAAM,SAAS,CAAC,GAAG,MAAM,MAAM,MAAM,QAAQ,EAAE,MAAM;IACrD,SAAS;IACT,QAAQ,EAAE;IACX;;EAGH,KAAK,QAAQ;AACX,OAAI,MAAM,KAAK,WAAW,EACxB,QAAO;GAGT,MAAM,WAAW,MAAM,KAAK,MAAM,KAAK,SAAS;AAEhD,UAAO;IACL,MAAM,MAAM,KAAK,MAAM,GAAG,GAAG;IAC7B,SAAS;IAIT,QAAQ,UAAU,CAAC,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,MAAM;IAC3D;;EAGH,KAAK,QAAQ;AACX,OAAI,MAAM,OAAO,WAAW,EAC1B,QAAO;GAGT,MAAM,CAAC,MAAM,GAAG,QAAQ,MAAM;AAE9B,UAAO;IACL,MAAM,SAAS,CAAC,GAAG,MAAM,MAAM,MAAM,QAAQ,EAAE,MAAM;IACrD,SAAS;IACT,QAAQ;IACT;;EAGH,KAAK,QACH,QAAO;GAAE,MAAM,EAAE;GAAE,SAAS,OAAO;GAAO,QAAQ,EAAE;GAAE;EAGxD,QACE,QAAO;;;AASf,MAAM,gBAAgB;AAEtB,MAAM,mBACJ,cACA,YACG;CAGH,MAAM,CAAC,OAAO,YAAY,WAAW,cAFvB,SAAS,SAAS,cAE4B,EAAE;EAC5D,MAAM,EAAE;EACR,SAAS;EACT,QAAQ,EAAE;EACX,CAAC;CAEF,MAAM,WAAW,aAAa,UAAgC;AAC5D,WAAS;GAAE,MAAM;GAAO;GAAO,CAAC;IAC/B,EAAE,CAAC;CAEN,MAAM,OAAO,kBAAkB;AAC7B,WAAS,EAAE,MAAM,QAAQ,CAAC;IACzB,EAAE,CAAC;CAEN,MAAM,OAAO,kBAAkB;AAC7B,WAAS,EAAE,MAAM,QAAQ,CAAC;IACzB,EAAE,CAAC;CAEN,MAAM,QAAQ,aAAa,UAAa;AACtC,WAAS;GAAE,MAAM;GAAS;GAAO,CAAC;IACjC,EAAE,CAAC;AAEN,QAAO;EACL,OAAO,MAAM;EACb;EACA;EACA;EACA;EACA,SAAS,MAAM,KAAK,SAAS;EAC7B,SAAS,MAAM,OAAO,SAAS;EAChC"}
|
|
@@ -1,25 +1,31 @@
|
|
|
1
|
-
import { useCallback, useRef } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-scroll-to-elements.ts
|
|
4
4
|
const useScrollToElements = (options) => {
|
|
5
5
|
const elementRefs = useRef([]);
|
|
6
|
+
const optionsRef = useRef(options);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
optionsRef.current = options;
|
|
9
|
+
});
|
|
6
10
|
const scrollToElement = useCallback((index) => {
|
|
7
11
|
const element = elementRefs.current[index];
|
|
8
12
|
if (!element) return;
|
|
13
|
+
const { offset, ...scrollIntoViewOptions } = optionsRef.current ?? {};
|
|
14
|
+
if (offset != null) {
|
|
15
|
+
const top = element.getBoundingClientRect().top + window.scrollY - offset;
|
|
16
|
+
window.scrollTo({
|
|
17
|
+
top,
|
|
18
|
+
behavior: scrollIntoViewOptions.behavior || "smooth"
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
9
22
|
element.scrollIntoView({
|
|
10
23
|
behavior: "smooth",
|
|
11
24
|
block: "start",
|
|
12
25
|
inline: "start",
|
|
13
|
-
...
|
|
26
|
+
...scrollIntoViewOptions
|
|
14
27
|
});
|
|
15
|
-
|
|
16
|
-
const top = element.getBoundingClientRect().top + window.scrollY - options.offset;
|
|
17
|
-
window.scrollTo({
|
|
18
|
-
top,
|
|
19
|
-
behavior: options.behavior || "smooth"
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
}, [options]);
|
|
28
|
+
}, []);
|
|
23
29
|
return {
|
|
24
30
|
elementRefs,
|
|
25
31
|
setElementRef: useCallback((element, index) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-scroll-to-elements.mjs","names":[],"sources":["../../src/hooks/use-scroll-to-elements.ts"],"sourcesContent":["import { useCallback, useRef } from 'react';\n\ninterface Options extends ScrollIntoViewOptions {\n offset?: number;\n}\nconst useScrollToElements = (options?: Options) => {\n const elementRefs = useRef<(HTMLElement | null)[]>([]);\n\n const
|
|
1
|
+
{"version":3,"file":"use-scroll-to-elements.mjs","names":[],"sources":["../../src/hooks/use-scroll-to-elements.ts"],"sourcesContent":["import { useCallback, useEffect, useRef } from 'react';\n\ninterface Options extends ScrollIntoViewOptions {\n offset?: number;\n}\n\nconst useScrollToElements = (options?: Options) => {\n const elementRefs = useRef<(HTMLElement | null)[]>([]);\n // `options` is almost always an inline object literal at the call site,\n // so depending on it directly would recreate `scrollToElement` on every\n // render — read the latest value from a ref instead.\n const optionsRef = useRef(options);\n\n useEffect(() => {\n optionsRef.current = options;\n });\n\n const scrollToElement = useCallback((index: number) => {\n const element = elementRefs.current[index];\n\n if (!element) {\n return;\n }\n\n const { offset, ...scrollIntoViewOptions } = optionsRef.current ?? {};\n\n // With an offset, `scrollIntoView` and `scrollTo` would both animate\n // the same scroll, fighting each other and settling in the wrong\n // place — use `scrollTo` alone, computed from the element's position\n // before any scrolling starts (not after `scrollIntoView`, where the\n // rect would reflect a mid-animation position).\n if (offset != null) {\n const top = element.getBoundingClientRect().top + window.scrollY - offset;\n\n window.scrollTo({\n top,\n behavior: scrollIntoViewOptions.behavior || 'smooth',\n });\n return;\n }\n\n element.scrollIntoView({\n behavior: 'smooth',\n block: 'start',\n inline: 'start',\n ...scrollIntoViewOptions,\n });\n }, []);\n\n const setElementRef = useCallback(\n (element: HTMLElement | null, index: number) => {\n elementRefs.current[index] = element;\n },\n [],\n );\n\n return { elementRefs, setElementRef, scrollToElement };\n};\n\nexport default useScrollToElements;\n"],"mappings":";;;AAMA,MAAM,uBAAuB,YAAsB;CACjD,MAAM,cAAc,OAA+B,EAAE,CAAC;CAItD,MAAM,aAAa,OAAO,QAAQ;AAElC,iBAAgB;AACd,aAAW,UAAU;GACrB;CAEF,MAAM,kBAAkB,aAAa,UAAkB;EACrD,MAAM,UAAU,YAAY,QAAQ;AAEpC,MAAI,CAAC,QACH;EAGF,MAAM,EAAE,QAAQ,GAAG,0BAA0B,WAAW,WAAW,EAAE;AAOrE,MAAI,UAAU,MAAM;GAClB,MAAM,MAAM,QAAQ,uBAAuB,CAAC,MAAM,OAAO,UAAU;AAEnE,UAAO,SAAS;IACd;IACA,UAAU,sBAAsB,YAAY;IAC7C,CAAC;AACF;;AAGF,UAAQ,eAAe;GACrB,UAAU;GACV,OAAO;GACP,QAAQ;GACR,GAAG;GACJ,CAAC;IACD,EAAE,CAAC;AASN,QAAO;EAAE;EAAa,eAPA,aACnB,SAA6B,UAAkB;AAC9C,eAAY,QAAQ,SAAS;KAE/B,EAAE,CACH;EAEoC;EAAiB"}
|