@publikit/hooks 0.1.1 → 1.0.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/CHANGELOG.md +24 -0
- package/README.md +28 -5
- package/dist/index.cjs +113 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -74
- package/dist/index.d.ts +7 -74
- package/dist/index.js +111 -67
- package/dist/index.js.map +1 -1
- package/dist/use-infinite-scroll.cjs +53 -0
- package/dist/use-infinite-scroll.cjs.map +1 -0
- package/dist/use-infinite-scroll.d.cts +28 -0
- package/dist/use-infinite-scroll.d.ts +28 -0
- package/dist/use-infinite-scroll.js +51 -0
- package/dist/use-infinite-scroll.js.map +1 -0
- package/dist/use-live-timestamp.cjs +156 -0
- package/dist/use-live-timestamp.cjs.map +1 -0
- package/dist/use-live-timestamp.d.cts +6 -0
- package/dist/use-live-timestamp.d.ts +6 -0
- package/dist/use-live-timestamp.js +154 -0
- package/dist/use-live-timestamp.js.map +1 -0
- package/dist/use-media-query.cjs +24 -0
- package/dist/use-media-query.cjs.map +1 -0
- package/dist/use-media-query.d.cts +10 -0
- package/dist/use-media-query.d.ts +10 -0
- package/dist/use-media-query.js +22 -0
- package/dist/use-media-query.js.map +1 -0
- package/dist/use-mobile.cjs +37 -0
- package/dist/use-mobile.cjs.map +1 -0
- package/dist/use-mobile.d.cts +10 -0
- package/dist/use-mobile.d.ts +10 -0
- package/dist/use-mobile.js +34 -0
- package/dist/use-mobile.js.map +1 -0
- package/dist/use-pull-to-refresh.cjs +103 -0
- package/dist/use-pull-to-refresh.cjs.map +1 -0
- package/dist/use-pull-to-refresh.d.cts +22 -0
- package/dist/use-pull-to-refresh.d.ts +22 -0
- package/dist/use-pull-to-refresh.js +101 -0
- package/dist/use-pull-to-refresh.js.map +1 -0
- package/dist/use-scrollable-container.cjs +149 -0
- package/dist/use-scrollable-container.cjs.map +1 -0
- package/dist/use-scrollable-container.d.cts +38 -0
- package/dist/use-scrollable-container.d.ts +38 -0
- package/dist/use-scrollable-container.js +144 -0
- package/dist/use-scrollable-container.js.map +1 -0
- package/package.json +38 -5
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useRef, useState, useMemo, useCallback, useLayoutEffect, useEffect } from 'react';
|
|
2
|
+
import { debounce } from '@publikit/utils';
|
|
3
|
+
|
|
4
|
+
// use-scrollable-container.ts
|
|
5
|
+
var SCROLL_AMOUNT = 80;
|
|
6
|
+
var SCROLL_CHECK_DELAY = 100;
|
|
7
|
+
var DEFAULT_SCROLL_POSITION_KEY = "scrollable-container-position";
|
|
8
|
+
var STORAGE_DEBOUNCE_MS = 150;
|
|
9
|
+
function resolveStorage(storage) {
|
|
10
|
+
if (storage === false) return null;
|
|
11
|
+
if (storage) return storage;
|
|
12
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
13
|
+
return sessionStorage;
|
|
14
|
+
}
|
|
15
|
+
function getScrollPosition(element, axis) {
|
|
16
|
+
return axis === "x" ? element.scrollLeft : element.scrollTop;
|
|
17
|
+
}
|
|
18
|
+
function setScrollPosition(element, axis, position) {
|
|
19
|
+
if (axis === "x") {
|
|
20
|
+
element.scrollLeft = position;
|
|
21
|
+
} else {
|
|
22
|
+
element.scrollTop = position;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function getScrollSize(element, axis) {
|
|
26
|
+
return axis === "x" ? element.scrollWidth : element.scrollHeight;
|
|
27
|
+
}
|
|
28
|
+
function getClientSize(element, axis) {
|
|
29
|
+
return axis === "x" ? element.clientWidth : element.clientHeight;
|
|
30
|
+
}
|
|
31
|
+
function useScrollableContainer({
|
|
32
|
+
scrollPositionKey,
|
|
33
|
+
axis = "y",
|
|
34
|
+
scrollAmount = SCROLL_AMOUNT,
|
|
35
|
+
checkDelay = SCROLL_CHECK_DELAY,
|
|
36
|
+
storage
|
|
37
|
+
}) {
|
|
38
|
+
const containerRef = useRef(null);
|
|
39
|
+
const [containerElement, setContainerElement] = useState(null);
|
|
40
|
+
const isRestoringRef = useRef(false);
|
|
41
|
+
const [canScrollBackward, setCanScrollBackward] = useState(false);
|
|
42
|
+
const [canScrollForward, setCanScrollForward] = useState(false);
|
|
43
|
+
const storageAdapter = useMemo(() => resolveStorage(storage), [storage]);
|
|
44
|
+
const assignContainerRef = useCallback((node) => {
|
|
45
|
+
containerRef.current = node;
|
|
46
|
+
setContainerElement(node);
|
|
47
|
+
}, []);
|
|
48
|
+
const saveToStorage = useMemo(
|
|
49
|
+
() => debounce((position) => {
|
|
50
|
+
if (!storageAdapter) return;
|
|
51
|
+
try {
|
|
52
|
+
storageAdapter.setItem(scrollPositionKey, String(position));
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}, STORAGE_DEBOUNCE_MS),
|
|
56
|
+
[scrollPositionKey, storageAdapter]
|
|
57
|
+
);
|
|
58
|
+
const checkScrollability = useCallback(() => {
|
|
59
|
+
if (!containerRef.current || isRestoringRef.current) return;
|
|
60
|
+
const scrollPosition = getScrollPosition(containerRef.current, axis);
|
|
61
|
+
const scrollSize = getScrollSize(containerRef.current, axis);
|
|
62
|
+
const clientSize = getClientSize(containerRef.current, axis);
|
|
63
|
+
saveToStorage(scrollPosition);
|
|
64
|
+
setCanScrollBackward(scrollPosition > 0);
|
|
65
|
+
setCanScrollForward(scrollPosition < scrollSize - clientSize - 1);
|
|
66
|
+
}, [axis, saveToStorage]);
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
if (!containerElement || isRestoringRef.current) return;
|
|
69
|
+
if (!storageAdapter) return;
|
|
70
|
+
try {
|
|
71
|
+
const savedPosition = storageAdapter.getItem(scrollPositionKey);
|
|
72
|
+
if (savedPosition !== null) {
|
|
73
|
+
const position = parseInt(savedPosition, 10);
|
|
74
|
+
const currentPosition = getScrollPosition(containerElement, axis);
|
|
75
|
+
if (position > 0 && !isNaN(position) && Math.abs(currentPosition - position) > 1) {
|
|
76
|
+
isRestoringRef.current = true;
|
|
77
|
+
setScrollPosition(containerElement, axis, position);
|
|
78
|
+
requestAnimationFrame(() => {
|
|
79
|
+
isRestoringRef.current = false;
|
|
80
|
+
checkScrollability();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}, [axis, containerElement, scrollPositionKey, checkScrollability, storageAdapter]);
|
|
87
|
+
const preserveScrollPosition = useCallback(() => {
|
|
88
|
+
if (containerRef.current && storageAdapter) {
|
|
89
|
+
try {
|
|
90
|
+
storageAdapter.setItem(
|
|
91
|
+
scrollPositionKey,
|
|
92
|
+
String(getScrollPosition(containerRef.current, axis))
|
|
93
|
+
);
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}, [axis, scrollPositionKey, storageAdapter]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
return () => {
|
|
100
|
+
saveToStorage.cancel();
|
|
101
|
+
};
|
|
102
|
+
}, [saveToStorage]);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!containerElement) return;
|
|
105
|
+
const timeoutId = setTimeout(checkScrollability, checkDelay);
|
|
106
|
+
const handleScroll = () => {
|
|
107
|
+
checkScrollability();
|
|
108
|
+
};
|
|
109
|
+
containerElement.addEventListener("scroll", handleScroll, { passive: true });
|
|
110
|
+
window.addEventListener("resize", checkScrollability);
|
|
111
|
+
const resizeObserver = new ResizeObserver(checkScrollability);
|
|
112
|
+
resizeObserver.observe(containerElement);
|
|
113
|
+
return () => {
|
|
114
|
+
clearTimeout(timeoutId);
|
|
115
|
+
containerElement.removeEventListener("scroll", handleScroll);
|
|
116
|
+
window.removeEventListener("resize", checkScrollability);
|
|
117
|
+
resizeObserver.disconnect();
|
|
118
|
+
};
|
|
119
|
+
}, [checkDelay, containerElement, checkScrollability]);
|
|
120
|
+
const scrollBackward = useCallback(() => {
|
|
121
|
+
const delta = -scrollAmount;
|
|
122
|
+
containerRef.current?.scrollBy(
|
|
123
|
+
axis === "x" ? { left: delta, behavior: "smooth" } : { top: delta, behavior: "smooth" }
|
|
124
|
+
);
|
|
125
|
+
}, [axis, scrollAmount]);
|
|
126
|
+
const scrollForward = useCallback(() => {
|
|
127
|
+
containerRef.current?.scrollBy(
|
|
128
|
+
axis === "x" ? { left: scrollAmount, behavior: "smooth" } : { top: scrollAmount, behavior: "smooth" }
|
|
129
|
+
);
|
|
130
|
+
}, [axis, scrollAmount]);
|
|
131
|
+
return {
|
|
132
|
+
containerRef: assignContainerRef,
|
|
133
|
+
canScrollBackward,
|
|
134
|
+
canScrollForward,
|
|
135
|
+
scrollBackward,
|
|
136
|
+
scrollForward,
|
|
137
|
+
checkScrollability,
|
|
138
|
+
preserveScrollPosition
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { DEFAULT_SCROLL_POSITION_KEY, SCROLL_AMOUNT, SCROLL_CHECK_DELAY, useScrollableContainer };
|
|
143
|
+
//# sourceMappingURL=use-scrollable-container.js.map
|
|
144
|
+
//# sourceMappingURL=use-scrollable-container.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../use-scrollable-container.ts"],"names":[],"mappings":"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","file":"use-scrollable-container.js","sourcesContent":["import { useRef, useEffect, useLayoutEffect, useState, useCallback, useMemo, type Ref } from 'react';\r\nimport { debounce } from '@publikit/utils';\r\n\r\nexport const SCROLL_AMOUNT = 80;\r\nexport const SCROLL_CHECK_DELAY = 100;\r\nexport const DEFAULT_SCROLL_POSITION_KEY = 'scrollable-container-position';\r\nconst STORAGE_DEBOUNCE_MS = 150;\r\n\r\nexport type ScrollAxis = 'x' | 'y';\r\n\r\nexport interface ScrollStorageAdapter {\r\n getItem(key: string): string | null;\r\n setItem(key: string, value: string): void;\r\n}\r\n\r\nexport interface UseScrollableContainerOptions {\r\n /** Session/storage key used to persist scroll position. */\r\n scrollPositionKey: string;\r\n /** Axis to measure and scroll. */\r\n axis?: ScrollAxis;\r\n /** Distance used by scroll helpers. */\r\n scrollAmount?: number;\r\n /** Delay before initial scrollability check. */\r\n checkDelay?: number;\r\n /** Optional storage adapter. Pass `false` to disable persistence. */\r\n storage?: ScrollStorageAdapter | false;\r\n}\r\n\r\nexport interface UseScrollableContainerReturn {\r\n /** Callback ref — attach to the scrollable element. */\r\n containerRef: Ref<HTMLElement | null>;\r\n canScrollBackward: boolean;\r\n canScrollForward: boolean;\r\n scrollBackward: () => void;\r\n scrollForward: () => void;\r\n checkScrollability: () => void;\r\n preserveScrollPosition: () => void;\r\n}\r\n\r\nfunction resolveStorage(\r\n storage: ScrollStorageAdapter | false | undefined,\r\n): ScrollStorageAdapter | null {\r\n if (storage === false) return null;\r\n if (storage) return storage;\r\n if (typeof sessionStorage === 'undefined') return null;\r\n return sessionStorage;\r\n}\r\n\r\nfunction getScrollPosition(element: HTMLElement, axis: ScrollAxis): number {\r\n return axis === 'x' ? element.scrollLeft : element.scrollTop;\r\n}\r\n\r\nfunction setScrollPosition(element: HTMLElement, axis: ScrollAxis, position: number): void {\r\n if (axis === 'x') {\r\n element.scrollLeft = position;\r\n } else {\r\n element.scrollTop = position;\r\n }\r\n}\r\n\r\nfunction getScrollSize(element: HTMLElement, axis: ScrollAxis): number {\r\n return axis === 'x' ? element.scrollWidth : element.scrollHeight;\r\n}\r\n\r\nfunction getClientSize(element: HTMLElement, axis: ScrollAxis): number {\r\n return axis === 'x' ? element.clientWidth : element.clientHeight;\r\n}\r\n\r\n/**\r\n * Scrollable container with position persistence and scroll button state.\r\n */\r\nexport function useScrollableContainer(\r\n {\r\n scrollPositionKey,\r\n axis = 'y',\r\n scrollAmount = SCROLL_AMOUNT,\r\n checkDelay = SCROLL_CHECK_DELAY,\r\n storage,\r\n }: UseScrollableContainerOptions,\r\n): UseScrollableContainerReturn {\r\n const containerRef = useRef<HTMLElement | null>(null);\r\n const [containerElement, setContainerElement] = useState<HTMLElement | null>(null);\r\n const isRestoringRef = useRef<boolean>(false);\r\n const [canScrollBackward, setCanScrollBackward] = useState(false);\r\n const [canScrollForward, setCanScrollForward] = useState(false);\r\n const storageAdapter = useMemo(() => resolveStorage(storage), [storage]);\r\n\r\n const assignContainerRef = useCallback((node: HTMLElement | null) => {\r\n containerRef.current = node;\r\n setContainerElement(node);\r\n }, []);\r\n\r\n const saveToStorage = useMemo(\r\n () =>\r\n debounce((position: number) => {\r\n if (!storageAdapter) return;\r\n try {\r\n storageAdapter.setItem(scrollPositionKey, String(position));\r\n } catch {\r\n // Ignore storage errors\r\n }\r\n }, STORAGE_DEBOUNCE_MS),\r\n [scrollPositionKey, storageAdapter]\r\n );\r\n\r\n const checkScrollability = useCallback(() => {\r\n if (!containerRef.current || isRestoringRef.current) return;\r\n const scrollPosition = getScrollPosition(containerRef.current, axis);\r\n const scrollSize = getScrollSize(containerRef.current, axis);\r\n const clientSize = getClientSize(containerRef.current, axis);\r\n\r\n saveToStorage(scrollPosition);\r\n\r\n setCanScrollBackward(scrollPosition > 0);\r\n setCanScrollForward(scrollPosition < scrollSize - clientSize - 1);\r\n }, [axis, saveToStorage]);\r\n\r\n useLayoutEffect(() => {\r\n if (!containerElement || isRestoringRef.current) return;\r\n if (!storageAdapter) return;\r\n\r\n try {\r\n const savedPosition = storageAdapter.getItem(scrollPositionKey);\r\n if (savedPosition !== null) {\r\n const position = parseInt(savedPosition, 10);\r\n const currentPosition = getScrollPosition(containerElement, axis);\r\n\r\n if (position > 0 && !isNaN(position) && Math.abs(currentPosition - position) > 1) {\r\n isRestoringRef.current = true;\r\n setScrollPosition(containerElement, axis, position);\r\n requestAnimationFrame(() => {\r\n isRestoringRef.current = false;\r\n checkScrollability();\r\n });\r\n }\r\n }\r\n } catch {\r\n // Ignore storage errors\r\n }\r\n }, [axis, containerElement, scrollPositionKey, checkScrollability, storageAdapter]);\r\n\r\n const preserveScrollPosition = useCallback(() => {\r\n if (containerRef.current && storageAdapter) {\r\n try {\r\n storageAdapter.setItem(\r\n scrollPositionKey,\r\n String(getScrollPosition(containerRef.current, axis)),\r\n );\r\n } catch {\r\n // Ignore storage errors\r\n }\r\n }\r\n }, [axis, scrollPositionKey, storageAdapter]);\r\n\r\n useEffect(() => {\r\n return () => {\r\n saveToStorage.cancel();\r\n };\r\n }, [saveToStorage]);\r\n\r\n useEffect(() => {\r\n if (!containerElement) return;\r\n\r\n const timeoutId = setTimeout(checkScrollability, checkDelay);\r\n\r\n const handleScroll = () => {\r\n checkScrollability();\r\n };\r\n\r\n containerElement.addEventListener('scroll', handleScroll, { passive: true });\r\n window.addEventListener('resize', checkScrollability);\r\n\r\n const resizeObserver = new ResizeObserver(checkScrollability);\r\n resizeObserver.observe(containerElement);\r\n\r\n return () => {\r\n clearTimeout(timeoutId);\r\n containerElement.removeEventListener('scroll', handleScroll);\r\n window.removeEventListener('resize', checkScrollability);\r\n resizeObserver.disconnect();\r\n };\r\n }, [checkDelay, containerElement, checkScrollability]);\r\n\r\n const scrollBackward = useCallback(() => {\r\n const delta = -scrollAmount;\r\n containerRef.current?.scrollBy(\r\n axis === 'x' ? { left: delta, behavior: 'smooth' } : { top: delta, behavior: 'smooth' },\r\n );\r\n }, [axis, scrollAmount]);\r\n\r\n const scrollForward = useCallback(() => {\r\n containerRef.current?.scrollBy(\r\n axis === 'x'\r\n ? { left: scrollAmount, behavior: 'smooth' }\r\n : { top: scrollAmount, behavior: 'smooth' },\r\n );\r\n }, [axis, scrollAmount]);\r\n\r\n return {\r\n containerRef: assignContainerRef as Ref<HTMLElement | null>,\r\n canScrollBackward,\r\n canScrollForward,\r\n scrollBackward,\r\n scrollForward,\r\n checkScrollability,\r\n preserveScrollPosition,\r\n };\r\n}\r\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@publikit/hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Generic, reusable React hooks for Publikit packages and applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -11,11 +11,42 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js",
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./use-media-query": {
|
|
16
|
+
"types": "./dist/use-media-query.d.ts",
|
|
17
|
+
"import": "./dist/use-media-query.js",
|
|
18
|
+
"require": "./dist/use-media-query.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./use-mobile": {
|
|
21
|
+
"types": "./dist/use-mobile.d.ts",
|
|
22
|
+
"import": "./dist/use-mobile.js",
|
|
23
|
+
"require": "./dist/use-mobile.cjs"
|
|
24
|
+
},
|
|
25
|
+
"./use-infinite-scroll": {
|
|
26
|
+
"types": "./dist/use-infinite-scroll.d.ts",
|
|
27
|
+
"import": "./dist/use-infinite-scroll.js",
|
|
28
|
+
"require": "./dist/use-infinite-scroll.cjs"
|
|
29
|
+
},
|
|
30
|
+
"./use-pull-to-refresh": {
|
|
31
|
+
"types": "./dist/use-pull-to-refresh.d.ts",
|
|
32
|
+
"import": "./dist/use-pull-to-refresh.js",
|
|
33
|
+
"require": "./dist/use-pull-to-refresh.cjs"
|
|
34
|
+
},
|
|
35
|
+
"./use-live-timestamp": {
|
|
36
|
+
"types": "./dist/use-live-timestamp.d.ts",
|
|
37
|
+
"import": "./dist/use-live-timestamp.js",
|
|
38
|
+
"require": "./dist/use-live-timestamp.cjs"
|
|
39
|
+
},
|
|
40
|
+
"./use-scrollable-container": {
|
|
41
|
+
"types": "./dist/use-scrollable-container.d.ts",
|
|
42
|
+
"import": "./dist/use-scrollable-container.js",
|
|
43
|
+
"require": "./dist/use-scrollable-container.cjs"
|
|
14
44
|
}
|
|
15
45
|
},
|
|
16
46
|
"files": [
|
|
17
47
|
"dist",
|
|
18
48
|
"README.md",
|
|
49
|
+
"CHANGELOG.md",
|
|
19
50
|
"LICENSE"
|
|
20
51
|
],
|
|
21
52
|
"sideEffects": false,
|
|
@@ -25,7 +56,8 @@
|
|
|
25
56
|
"clean": "rimraf dist",
|
|
26
57
|
"prepublishOnly": "npm run clean && npm run build",
|
|
27
58
|
"typecheck": "tsc --noEmit",
|
|
28
|
-
"test": "vitest run"
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:coverage": "vitest run --coverage"
|
|
29
61
|
},
|
|
30
62
|
"keywords": [
|
|
31
63
|
"react",
|
|
@@ -39,9 +71,9 @@
|
|
|
39
71
|
"repository": {
|
|
40
72
|
"type": "git",
|
|
41
73
|
"url": "git+https://github.com/pirimera/publikit.git",
|
|
42
|
-
"directory": "hooks"
|
|
74
|
+
"directory": "packages/hooks"
|
|
43
75
|
},
|
|
44
|
-
"homepage": "https://github.com/pirimera/publikit/tree/main/hooks#readme",
|
|
76
|
+
"homepage": "https://github.com/pirimera/publikit/tree/main/packages/hooks#readme",
|
|
45
77
|
"bugs": {
|
|
46
78
|
"url": "https://github.com/pirimera/publikit/issues"
|
|
47
79
|
},
|
|
@@ -55,7 +87,7 @@
|
|
|
55
87
|
"react": "^18.0.0 || ^19.0.0"
|
|
56
88
|
},
|
|
57
89
|
"dependencies": {
|
|
58
|
-
"@publikit/utils": "^0.
|
|
90
|
+
"@publikit/utils": "^1.0.0"
|
|
59
91
|
},
|
|
60
92
|
"devDependencies": {
|
|
61
93
|
"@publikit/utils": "file:../utils",
|
|
@@ -68,6 +100,7 @@
|
|
|
68
100
|
"rimraf": "^5.0.0",
|
|
69
101
|
"tsup": "^8.0.0",
|
|
70
102
|
"typescript": "^5.8.3",
|
|
103
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
71
104
|
"vitest": "^4.0.18"
|
|
72
105
|
}
|
|
73
106
|
}
|