@inkra/react 0.8.0 → 0.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/README.md +137 -2
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/Xiaolai-OFL.txt +94 -0
- package/dist/PencilIcon.d.ts +14 -1
- package/dist/PencilIcon.js +88 -52
- package/dist/PencilText.js +4 -1
- package/dist/assets/ui-hand-xiaolai-extended.woff2 +0 -0
- package/dist/assets/ui-hand-xiaolai.woff2 +0 -0
- package/dist/assets/xiaolai.css +17 -0
- package/dist/form/EditablePencilText.js +4 -2
- package/dist/guide/index.d.ts +77 -0
- package/dist/guide/index.js +379 -0
- package/dist/i18n/catalog.d.ts +1 -1
- package/dist/i18n/catalog.js +8 -0
- package/dist/image-viewer/index.d.ts +2 -0
- package/dist/image-viewer/index.js +28 -28
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/infinite-scroll/gesture.d.ts +19 -0
- package/dist/infinite-scroll/gesture.js +218 -0
- package/dist/infinite-scroll/index.d.ts +55 -0
- package/dist/infinite-scroll/index.js +543 -0
- package/dist/infinite-scroll/model.d.ts +33 -0
- package/dist/infinite-scroll/model.js +168 -0
- package/dist/internal/iconFill.d.ts +16 -0
- package/dist/internal/iconFill.js +275 -0
- package/dist/internal/surfaceRenderer.d.ts +4 -4
- package/dist/internal/surfaceRenderer.js +2 -2
- package/dist/internal/textRenderer.d.ts +1 -1
- package/dist/internal/textRenderer.js +23 -11
- package/dist/inventory-grid/index.js +13 -3
- package/dist/inventory-grid/outline.d.ts +9 -0
- package/dist/inventory-grid/outline.js +54 -0
- package/dist/loading/index.d.ts +58 -0
- package/dist/loading/index.js +188 -0
- package/dist/loading/locales.d.ts +15 -0
- package/dist/loading/locales.js +34 -0
- package/dist/loading/model.d.ts +6 -0
- package/dist/loading/model.js +29 -0
- package/dist/masonry/index.d.ts +25 -0
- package/dist/masonry/index.js +227 -0
- package/dist/masonry/layout.d.ts +35 -0
- package/dist/masonry/layout.js +67 -0
- package/dist/number-ticker/RollingValue.d.ts +6 -0
- package/dist/number-ticker/RollingValue.js +10 -0
- package/dist/number-ticker/index.d.ts +4 -0
- package/dist/number-ticker/index.js +55 -7
- package/dist/number-ticker/rolling.d.ts +35 -0
- package/dist/number-ticker/rolling.js +87 -0
- package/dist/selection/CircularSwitchVisual.d.ts +8 -0
- package/dist/selection/CircularSwitchVisual.js +82 -0
- package/dist/selection/Switch.d.ts +7 -0
- package/dist/selection/Switch.js +15 -3
- package/dist/slider/circular.d.ts +35 -0
- package/dist/slider/circular.js +196 -0
- package/dist/slider/circularMath.d.ts +10 -0
- package/dist/slider/circularMath.js +34 -0
- package/dist/slider/index.d.ts +5 -0
- package/dist/slider/index.js +5 -3
- package/dist/styles/base.css +4 -2
- package/dist/styles/breadcrumb.css +1 -1
- package/dist/styles/guide.css +202 -0
- package/dist/styles/image-viewer.css +20 -0
- package/dist/styles/infinite-scroll.css +171 -0
- package/dist/styles/inventory-grid.css +20 -8
- package/dist/styles/loading.css +348 -0
- package/dist/styles/markdown.css +1 -1
- package/dist/styles/masonry.css +68 -0
- package/dist/styles/number-ticker.css +47 -1
- package/dist/styles/selection.css +50 -0
- package/dist/styles/slider.css +65 -0
- package/dist/styles/slot.css +1 -1
- package/dist/styles.css +1180 -192
- package/dist/toast/manager.d.ts +4 -4
- package/dist/tokens.css +13 -10
- package/package.json +28 -2
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { Button } from '../Button.js';
|
|
4
|
+
import { Progress } from '../display/index.js';
|
|
5
|
+
import { useInkraLocale } from '../i18n/index.js';
|
|
6
|
+
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
7
|
+
import { PencilText } from '../PencilText.js';
|
|
8
|
+
import { createInfiniteScrollGesture } from './gesture.js';
|
|
9
|
+
import { boundarySignature, completeDirectionalRecord, dampPullDistance, directionalValue, finiteDistance, gestureStatusEnabled, indicatorVisible, infiniteScrollDirections, localScrollDelta, normalizeDirections, physicalBoundaryDistance, pullProgress, statusAfterCancellation, } from './model.js';
|
|
10
|
+
const localeCopy = {
|
|
11
|
+
'en-US': {
|
|
12
|
+
top: { idle: 'Load earlier items', pulling: 'Pull down to load', ready: 'Release to load', loading: 'Loading earlier items', error: 'Earlier items could not be loaded', end: 'Beginning reached' },
|
|
13
|
+
bottom: { idle: 'Load more items', pulling: 'Pull up to load', ready: 'Release to load', loading: 'Loading more items', error: 'More items could not be loaded', end: 'End reached' },
|
|
14
|
+
left: { idle: 'Load items on the left', pulling: 'Pull right to load', ready: 'Release to load', loading: 'Loading left items', error: 'Left items could not be loaded', end: 'Left edge reached' },
|
|
15
|
+
right: { idle: 'Load items on the right', pulling: 'Pull left to load', ready: 'Release to load', loading: 'Loading right items', error: 'Right items could not be loaded', end: 'Right edge reached' },
|
|
16
|
+
retry: 'Retry', loadMore: 'Load more',
|
|
17
|
+
},
|
|
18
|
+
'zh-CN': {
|
|
19
|
+
top: { idle: '加载更早内容', pulling: '下拉加载', ready: '松手加载', loading: '正在加载更早内容', error: '更早内容加载失败', end: '已到开头' },
|
|
20
|
+
bottom: { idle: '加载更多内容', pulling: '上拉加载', ready: '松手加载', loading: '正在加载更多内容', error: '更多内容加载失败', end: '已到末尾' },
|
|
21
|
+
left: { idle: '加载左侧内容', pulling: '向右拉动加载', ready: '松手加载', loading: '正在加载左侧内容', error: '左侧内容加载失败', end: '已到左侧边界' },
|
|
22
|
+
right: { idle: '加载右侧内容', pulling: '向左拉动加载', ready: '松手加载', loading: '正在加载右侧内容', error: '右侧内容加载失败', end: '已到右侧边界' },
|
|
23
|
+
retry: '重试', loadMore: '加载更多',
|
|
24
|
+
},
|
|
25
|
+
'ja-JP': {
|
|
26
|
+
top: { idle: '前の項目を読み込む', pulling: '下に引いて読み込む', ready: '離して読み込む', loading: '前の項目を読み込み中', error: '前の項目を読み込めませんでした', end: '先頭です' },
|
|
27
|
+
bottom: { idle: 'さらに読み込む', pulling: '上に引いて読み込む', ready: '離して読み込む', loading: '読み込み中', error: '読み込めませんでした', end: '末尾です' },
|
|
28
|
+
left: { idle: '左側を読み込む', pulling: '右に引いて読み込む', ready: '離して読み込む', loading: '左側を読み込み中', error: '左側を読み込めませんでした', end: '左端です' },
|
|
29
|
+
right: { idle: '右側を読み込む', pulling: '左に引いて読み込む', ready: '離して読み込む', loading: '右側を読み込み中', error: '右側を読み込めませんでした', end: '右端です' },
|
|
30
|
+
retry: '再試行', loadMore: 'さらに読み込む',
|
|
31
|
+
},
|
|
32
|
+
'ko-KR': {
|
|
33
|
+
top: { idle: '이전 항목 불러오기', pulling: '아래로 당겨 불러오기', ready: '놓아서 불러오기', loading: '이전 항목 불러오는 중', error: '이전 항목을 불러오지 못했습니다', end: '처음입니다' },
|
|
34
|
+
bottom: { idle: '항목 더 불러오기', pulling: '위로 당겨 불러오기', ready: '놓아서 불러오기', loading: '불러오는 중', error: '항목을 불러오지 못했습니다', end: '마지막입니다' },
|
|
35
|
+
left: { idle: '왼쪽 항목 불러오기', pulling: '오른쪽으로 당겨 불러오기', ready: '놓아서 불러오기', loading: '왼쪽 항목 불러오는 중', error: '왼쪽 항목을 불러오지 못했습니다', end: '왼쪽 끝입니다' },
|
|
36
|
+
right: { idle: '오른쪽 항목 불러오기', pulling: '왼쪽으로 당겨 불러오기', ready: '놓아서 불러오기', loading: '오른쪽 항목 불러오는 중', error: '오른쪽 항목을 불러오지 못했습니다', end: '오른쪽 끝입니다' },
|
|
37
|
+
retry: '다시 시도', loadMore: '더 불러오기',
|
|
38
|
+
},
|
|
39
|
+
'fr-FR': {
|
|
40
|
+
top: { idle: 'Charger les éléments précédents', pulling: 'Tirez vers le bas pour charger', ready: 'Relâchez pour charger', loading: 'Chargement des éléments précédents', error: 'Impossible de charger les éléments précédents', end: 'Début atteint' },
|
|
41
|
+
bottom: { idle: 'Charger plus d’éléments', pulling: 'Tirez vers le haut pour charger', ready: 'Relâchez pour charger', loading: 'Chargement en cours', error: 'Impossible de charger plus d’éléments', end: 'Fin atteinte' },
|
|
42
|
+
left: { idle: 'Charger les éléments à gauche', pulling: 'Tirez vers la droite pour charger', ready: 'Relâchez pour charger', loading: 'Chargement à gauche', error: 'Impossible de charger les éléments à gauche', end: 'Bord gauche atteint' },
|
|
43
|
+
right: { idle: 'Charger les éléments à droite', pulling: 'Tirez vers la gauche pour charger', ready: 'Relâchez pour charger', loading: 'Chargement à droite', error: 'Impossible de charger les éléments à droite', end: 'Bord droit atteint' },
|
|
44
|
+
retry: 'Réessayer', loadMore: 'Charger plus',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
function copyLocale(locale) {
|
|
48
|
+
if (locale.toLowerCase().startsWith('zh'))
|
|
49
|
+
return localeCopy['zh-CN'];
|
|
50
|
+
if (locale.toLowerCase().startsWith('ja'))
|
|
51
|
+
return localeCopy['ja-JP'];
|
|
52
|
+
if (locale.toLowerCase().startsWith('ko'))
|
|
53
|
+
return localeCopy['ko-KR'];
|
|
54
|
+
if (locale.toLowerCase().startsWith('fr'))
|
|
55
|
+
return localeCopy['fr-FR'];
|
|
56
|
+
return localeCopy['en-US'];
|
|
57
|
+
}
|
|
58
|
+
function resolveContainer(value, root) {
|
|
59
|
+
if (typeof value === 'function')
|
|
60
|
+
return value();
|
|
61
|
+
return value ?? root;
|
|
62
|
+
}
|
|
63
|
+
function visibleAnchor(content, container, direction) {
|
|
64
|
+
const viewport = container.getBoundingClientRect();
|
|
65
|
+
const marked = Array.from(content.querySelectorAll('[data-infinite-scroll-anchor]'));
|
|
66
|
+
const candidates = marked.length ? marked : Array.from(content.children).filter((node) => node instanceof HTMLElement);
|
|
67
|
+
const visible = candidates.filter(node => {
|
|
68
|
+
const rect = node.getBoundingClientRect();
|
|
69
|
+
return direction === 'top'
|
|
70
|
+
? rect.bottom > viewport.top && rect.top < viewport.bottom
|
|
71
|
+
: rect.right > viewport.left && rect.left < viewport.right;
|
|
72
|
+
});
|
|
73
|
+
visible.sort((a, b) => {
|
|
74
|
+
const first = a.getBoundingClientRect();
|
|
75
|
+
const second = b.getBoundingClientRect();
|
|
76
|
+
return direction === 'top' ? first.top - second.top : first.left - second.left;
|
|
77
|
+
});
|
|
78
|
+
return visible[0] ?? null;
|
|
79
|
+
}
|
|
80
|
+
function createAnchorStabilizer(direction, container, content, getAnchor) {
|
|
81
|
+
const view = container.ownerDocument.defaultView;
|
|
82
|
+
let anchor = getAnchor?.({ direction, scrollContainer: container, content }) ?? visibleAnchor(content, container, direction);
|
|
83
|
+
if (!view || !anchor)
|
|
84
|
+
return { settle: async () => { }, abort: () => { } };
|
|
85
|
+
const key = anchor.dataset.infiniteScrollAnchor;
|
|
86
|
+
const offset = () => {
|
|
87
|
+
const viewport = container.getBoundingClientRect();
|
|
88
|
+
const rect = anchor?.getBoundingClientRect();
|
|
89
|
+
if (!rect)
|
|
90
|
+
return 0;
|
|
91
|
+
// Pull/rebound moves the body visually; anchoring must follow its layout position.
|
|
92
|
+
const body = content.parentElement;
|
|
93
|
+
const transform = body ? view.getComputedStyle(body).transform : 'none';
|
|
94
|
+
const matrix = transform && transform !== 'none' ? new view.DOMMatrixReadOnly(transform) : null;
|
|
95
|
+
const scale = direction === 'top'
|
|
96
|
+
? viewport.height / (container.offsetHeight || viewport.height || 1)
|
|
97
|
+
: viewport.width / (container.offsetWidth || viewport.width || 1);
|
|
98
|
+
const translation = direction === 'top' ? matrix?.m42 ?? 0 : matrix?.m41 ?? 0;
|
|
99
|
+
return (direction === 'top' ? rect.top - viewport.top : rect.left - viewport.left) - translation * scale;
|
|
100
|
+
};
|
|
101
|
+
let expected = offset();
|
|
102
|
+
let frame = 0;
|
|
103
|
+
let stopped = false;
|
|
104
|
+
let correcting = false;
|
|
105
|
+
let settling = false;
|
|
106
|
+
const findReplacement = () => {
|
|
107
|
+
if (anchor?.isConnected)
|
|
108
|
+
return anchor;
|
|
109
|
+
if (!key)
|
|
110
|
+
return null;
|
|
111
|
+
return Array.from(content.querySelectorAll('[data-infinite-scroll-anchor]'))
|
|
112
|
+
.find(node => node.dataset.infiniteScrollAnchor === key) ?? null;
|
|
113
|
+
};
|
|
114
|
+
const measure = () => {
|
|
115
|
+
frame = 0;
|
|
116
|
+
if (stopped || !settling)
|
|
117
|
+
return;
|
|
118
|
+
anchor = findReplacement();
|
|
119
|
+
if (!anchor)
|
|
120
|
+
return;
|
|
121
|
+
const current = offset();
|
|
122
|
+
const delta = current - expected;
|
|
123
|
+
if (Math.abs(delta) > 0.5) {
|
|
124
|
+
correcting = true;
|
|
125
|
+
const scrollDelta = localScrollDelta(container, direction, delta);
|
|
126
|
+
container.scrollBy(direction === 'top' ? { top: scrollDelta, behavior: 'auto' } : { left: scrollDelta, behavior: 'auto' });
|
|
127
|
+
expected = offset();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const schedule = () => {
|
|
131
|
+
if (settling && !frame && !stopped)
|
|
132
|
+
frame = view.requestAnimationFrame(measure);
|
|
133
|
+
};
|
|
134
|
+
const onScroll = () => {
|
|
135
|
+
if (correcting)
|
|
136
|
+
correcting = false;
|
|
137
|
+
anchor = findReplacement();
|
|
138
|
+
if (anchor)
|
|
139
|
+
expected = offset();
|
|
140
|
+
};
|
|
141
|
+
container.addEventListener('scroll', onScroll, { passive: true });
|
|
142
|
+
const mutation = typeof MutationObserver === 'undefined' ? null : new MutationObserver(schedule);
|
|
143
|
+
mutation?.observe(content, { childList: true, subtree: true });
|
|
144
|
+
const resize = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
|
|
145
|
+
resize?.observe(content);
|
|
146
|
+
resize?.observe(anchor);
|
|
147
|
+
const root = content.closest('.inkra-infinite-scroll');
|
|
148
|
+
if (root)
|
|
149
|
+
for (const child of Array.from(root.children))
|
|
150
|
+
resize?.observe(child);
|
|
151
|
+
const abort = () => {
|
|
152
|
+
if (stopped)
|
|
153
|
+
return;
|
|
154
|
+
stopped = true;
|
|
155
|
+
if (frame)
|
|
156
|
+
view.cancelAnimationFrame(frame);
|
|
157
|
+
container.removeEventListener('scroll', onScroll);
|
|
158
|
+
mutation?.disconnect();
|
|
159
|
+
resize?.disconnect();
|
|
160
|
+
};
|
|
161
|
+
const settle = () => new Promise(resolve => {
|
|
162
|
+
settling = true;
|
|
163
|
+
schedule();
|
|
164
|
+
view.requestAnimationFrame(() => view.requestAnimationFrame(() => {
|
|
165
|
+
measure();
|
|
166
|
+
abort();
|
|
167
|
+
resolve();
|
|
168
|
+
}));
|
|
169
|
+
});
|
|
170
|
+
return { settle, abort };
|
|
171
|
+
}
|
|
172
|
+
function explicitDirectionalBoolean(value, direction) {
|
|
173
|
+
if (value === true)
|
|
174
|
+
return true;
|
|
175
|
+
return typeof value === 'object' && value !== null && value[direction] === true;
|
|
176
|
+
}
|
|
177
|
+
export const InfiniteScroll = forwardRef(function InfiniteScroll(props, forwardedRef) {
|
|
178
|
+
const { automatic, children, className, defaultState, disabled, directions: directionsProp, errorContent, endContent, getAnchor, gesture, gestureThreshold, hasMore, labels, onLoad: loadHandler, onStateChange, renderIndicator, scrollContainer, state, style, systemGestureInset = 24, triggerDistance, onTouchStart, ...domProps } = props;
|
|
179
|
+
const { locale } = useInkraLocale();
|
|
180
|
+
const copy = copyLocale(locale);
|
|
181
|
+
const directions = useMemo(() => normalizeDirections(directionsProp), [directionsProp]);
|
|
182
|
+
const rootRef = useRef(null);
|
|
183
|
+
const contentRef = useRef(null);
|
|
184
|
+
const sentinelRefs = useRef({ top: null, bottom: null, left: null, right: null });
|
|
185
|
+
const [internalState, setInternalState] = useState(() => completeDirectionalRecord(defaultState, () => ({ status: 'idle' })));
|
|
186
|
+
const [pull, setPull] = useState({ direction: null, distance: 0, progress: 0 });
|
|
187
|
+
const propsRef = useRef(props);
|
|
188
|
+
const internalStateRef = useRef(internalState);
|
|
189
|
+
const requests = useRef(new Map());
|
|
190
|
+
const [completionVersion, setCompletionVersion] = useState(0);
|
|
191
|
+
const lastBoundary = useRef(new Map());
|
|
192
|
+
const scheduleBoundaryRef = useRef(() => { });
|
|
193
|
+
const pullStatus = useRef('idle');
|
|
194
|
+
const pullDirection = useRef(null);
|
|
195
|
+
const [containerNode, setContainerNode] = useState(null);
|
|
196
|
+
const containerNodeRef = useRef(null);
|
|
197
|
+
useBrowserLayoutEffect(() => { propsRef.current = props; internalStateRef.current = internalState; });
|
|
198
|
+
useBrowserLayoutEffect(() => {
|
|
199
|
+
for (const request of requests.current.values()) {
|
|
200
|
+
request.committed?.();
|
|
201
|
+
request.committed = undefined;
|
|
202
|
+
}
|
|
203
|
+
}, [completionVersion]);
|
|
204
|
+
const setRoot = useCallback((node) => {
|
|
205
|
+
rootRef.current = node;
|
|
206
|
+
if (typeof forwardedRef === 'function')
|
|
207
|
+
forwardedRef(node);
|
|
208
|
+
else if (forwardedRef)
|
|
209
|
+
forwardedRef.current = node;
|
|
210
|
+
}, [forwardedRef]);
|
|
211
|
+
const currentState = useCallback((direction) => propsRef.current.state?.[direction] ?? internalStateRef.current[direction], []);
|
|
212
|
+
const transition = useCallback((direction, next, details = {}) => {
|
|
213
|
+
setInternalState(current => {
|
|
214
|
+
if (propsRef.current.state?.[direction] !== undefined)
|
|
215
|
+
return current;
|
|
216
|
+
const updated = { ...current, [direction]: next };
|
|
217
|
+
internalStateRef.current = updated;
|
|
218
|
+
return updated;
|
|
219
|
+
});
|
|
220
|
+
propsRef.current.onStateChange?.(direction, next, details);
|
|
221
|
+
}, []);
|
|
222
|
+
const cancelDirection = useCallback((direction, reason) => {
|
|
223
|
+
const request = requests.current.get(direction);
|
|
224
|
+
request?.controller.abort();
|
|
225
|
+
request?.anchor.abort();
|
|
226
|
+
request?.committed?.();
|
|
227
|
+
requests.current.delete(direction);
|
|
228
|
+
const status = currentState(direction).status;
|
|
229
|
+
const nextStatus = statusAfterCancellation(status);
|
|
230
|
+
if (nextStatus !== status)
|
|
231
|
+
transition(direction, { status: nextStatus }, { source: request?.source, reason });
|
|
232
|
+
}, [currentState, transition]);
|
|
233
|
+
const runLoad = useCallback((direction, source) => {
|
|
234
|
+
const latest = propsRef.current;
|
|
235
|
+
const root = rootRef.current;
|
|
236
|
+
const content = contentRef.current;
|
|
237
|
+
const container = resolveContainer(latest.scrollContainer, root);
|
|
238
|
+
if (!root || !content || !container || requests.current.has(direction))
|
|
239
|
+
return;
|
|
240
|
+
if (directionalValue(latest.disabled, direction, false) || !directionalValue(latest.hasMore, direction, true))
|
|
241
|
+
return;
|
|
242
|
+
const status = currentState(direction).status;
|
|
243
|
+
if (status === 'loading' || status === 'end' || (status === 'error' && source === 'boundary'))
|
|
244
|
+
return;
|
|
245
|
+
const controller = new AbortController();
|
|
246
|
+
const anchor = direction === 'top' || direction === 'left'
|
|
247
|
+
? createAnchorStabilizer(direction, container, content, latest.getAnchor)
|
|
248
|
+
: { settle: async () => { }, abort: () => { } };
|
|
249
|
+
const revealPosition = direction === 'top' || direction === 'bottom' ? container.scrollTop : container.scrollLeft;
|
|
250
|
+
requests.current.set(direction, {
|
|
251
|
+
controller,
|
|
252
|
+
anchor,
|
|
253
|
+
container,
|
|
254
|
+
source,
|
|
255
|
+
revealed: false,
|
|
256
|
+
revealPosition,
|
|
257
|
+
});
|
|
258
|
+
transition(direction, { status: 'loading' }, { source });
|
|
259
|
+
scheduleBoundaryRef.current();
|
|
260
|
+
Promise.resolve().then(() => {
|
|
261
|
+
if (controller.signal.aborted)
|
|
262
|
+
return undefined;
|
|
263
|
+
return latest.onLoad({ direction, source, signal: controller.signal, scrollContainer: container });
|
|
264
|
+
})
|
|
265
|
+
.then(async () => {
|
|
266
|
+
if (controller.signal.aborted)
|
|
267
|
+
return;
|
|
268
|
+
// Async data and indicator state must reach the DOM before measuring anchors.
|
|
269
|
+
const committed = new Promise(resolve => {
|
|
270
|
+
const request = requests.current.get(direction);
|
|
271
|
+
if (request?.controller === controller)
|
|
272
|
+
request.committed = resolve;
|
|
273
|
+
else
|
|
274
|
+
resolve();
|
|
275
|
+
});
|
|
276
|
+
const more = directionalValue(propsRef.current.hasMore, direction, true);
|
|
277
|
+
transition(direction, { status: more ? 'idle' : 'end' }, { source });
|
|
278
|
+
setCompletionVersion(version => version + 1);
|
|
279
|
+
await committed;
|
|
280
|
+
if (controller.signal.aborted)
|
|
281
|
+
return;
|
|
282
|
+
await anchor.settle();
|
|
283
|
+
})
|
|
284
|
+
.catch(error => {
|
|
285
|
+
anchor.abort();
|
|
286
|
+
if (controller.signal.aborted || (error instanceof DOMException && error.name === 'AbortError'))
|
|
287
|
+
return;
|
|
288
|
+
transition(direction, { status: 'error', error }, { source, error });
|
|
289
|
+
})
|
|
290
|
+
.finally(() => {
|
|
291
|
+
const request = requests.current.get(direction);
|
|
292
|
+
if (request?.controller === controller)
|
|
293
|
+
requests.current.delete(direction);
|
|
294
|
+
scheduleBoundaryRef.current();
|
|
295
|
+
});
|
|
296
|
+
}, [currentState, transition]);
|
|
297
|
+
const gestureRef = useRef(null);
|
|
298
|
+
if (!gestureRef.current) {
|
|
299
|
+
gestureRef.current = createInfiniteScrollGesture(() => {
|
|
300
|
+
const latest = propsRef.current;
|
|
301
|
+
const root = rootRef.current;
|
|
302
|
+
return {
|
|
303
|
+
root,
|
|
304
|
+
container: resolveContainer(latest.scrollContainer, root),
|
|
305
|
+
directions: normalizeDirections(latest.directions),
|
|
306
|
+
isEnabled: direction => !requests.current.has(direction)
|
|
307
|
+
&& !directionalValue(latest.disabled, direction, false)
|
|
308
|
+
&& directionalValue(latest.hasMore, direction, true)
|
|
309
|
+
&& directionalValue(latest.gesture, direction, true)
|
|
310
|
+
&& gestureStatusEnabled(currentState(direction).status),
|
|
311
|
+
isExplicitlyEnabled: direction => explicitDirectionalBoolean(latest.gesture, direction),
|
|
312
|
+
threshold: direction => finiteDistance(directionalValue(latest.gestureThreshold, direction, 72), 72, 24),
|
|
313
|
+
sentinel: direction => sentinelRefs.current[direction],
|
|
314
|
+
systemGestureInset: finiteDistance(latest.systemGestureInset, 24),
|
|
315
|
+
onPull: (direction, distance, threshold, ready) => {
|
|
316
|
+
const nextStatus = direction ? (ready ? 'ready' : 'pulling') : 'idle';
|
|
317
|
+
if (direction && pullStatus.current !== nextStatus)
|
|
318
|
+
transition(direction, { status: nextStatus });
|
|
319
|
+
if (!direction && pullDirection.current && (pullStatus.current === 'pulling' || pullStatus.current === 'ready')) {
|
|
320
|
+
const status = currentState(pullDirection.current).status;
|
|
321
|
+
if (gestureStatusEnabled(status))
|
|
322
|
+
transition(pullDirection.current, { status: 'idle' });
|
|
323
|
+
}
|
|
324
|
+
pullStatus.current = nextStatus;
|
|
325
|
+
pullDirection.current = direction;
|
|
326
|
+
setPull({ direction, distance, progress: pullProgress(distance, dampPullDistance(threshold, threshold)) });
|
|
327
|
+
},
|
|
328
|
+
onCommit: direction => runLoad(direction, 'gesture'),
|
|
329
|
+
};
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
useBrowserLayoutEffect(() => {
|
|
333
|
+
const next = resolveContainer(scrollContainer, rootRef.current);
|
|
334
|
+
const previous = containerNodeRef.current;
|
|
335
|
+
if (next === previous)
|
|
336
|
+
return;
|
|
337
|
+
if (previous) {
|
|
338
|
+
for (const [direction, request] of requests.current) {
|
|
339
|
+
if (request.container === previous)
|
|
340
|
+
cancelDirection(direction, 'container-changed');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
containerNodeRef.current = next;
|
|
344
|
+
setContainerNode(next);
|
|
345
|
+
gestureRef.current?.cancel();
|
|
346
|
+
});
|
|
347
|
+
useBrowserLayoutEffect(() => {
|
|
348
|
+
const root = rootRef.current;
|
|
349
|
+
if (!root)
|
|
350
|
+
return;
|
|
351
|
+
const wheel = (event) => gestureRef.current?.wheel(event);
|
|
352
|
+
root.addEventListener('wheel', wheel, { passive: false });
|
|
353
|
+
return () => root.removeEventListener('wheel', wheel);
|
|
354
|
+
}, []);
|
|
355
|
+
useBrowserLayoutEffect(() => {
|
|
356
|
+
if (!pull.direction)
|
|
357
|
+
return;
|
|
358
|
+
const root = rootRef.current;
|
|
359
|
+
const container = resolveContainer(scrollContainer, root);
|
|
360
|
+
const indicator = sentinelRefs.current[pull.direction];
|
|
361
|
+
const parent = indicator?.offsetParent;
|
|
362
|
+
if (!root || !container || !indicator || !(parent instanceof HTMLElement))
|
|
363
|
+
return;
|
|
364
|
+
const viewport = container.getBoundingClientRect();
|
|
365
|
+
const origin = parent.getBoundingClientRect();
|
|
366
|
+
const scaleX = viewport.width / (container.offsetWidth || viewport.width || 1);
|
|
367
|
+
const scaleY = viewport.height / (container.offsetHeight || viewport.height || 1);
|
|
368
|
+
const horizontal = pull.direction === 'left' || pull.direction === 'right';
|
|
369
|
+
const width = horizontal ? pull.distance : container.clientWidth;
|
|
370
|
+
const height = horizontal ? container.clientHeight : pull.distance;
|
|
371
|
+
const left = (viewport.left - origin.left) / scaleX + container.clientLeft - parent.clientLeft + parent.scrollLeft
|
|
372
|
+
+ (pull.direction === 'right' ? container.clientWidth - width : 0);
|
|
373
|
+
const top = (viewport.top - origin.top) / scaleY + container.clientTop - parent.clientTop + parent.scrollTop
|
|
374
|
+
+ (pull.direction === 'bottom' ? container.clientHeight - height : 0);
|
|
375
|
+
indicator.style.setProperty('--inkra-pull-left', `${left}px`);
|
|
376
|
+
indicator.style.setProperty('--inkra-pull-top', `${top}px`);
|
|
377
|
+
indicator.style.setProperty('--inkra-pull-width', `${width}px`);
|
|
378
|
+
indicator.style.setProperty('--inkra-pull-height', `${height}px`);
|
|
379
|
+
}, [pull, scrollContainer]);
|
|
380
|
+
useBrowserLayoutEffect(() => {
|
|
381
|
+
const root = rootRef.current;
|
|
382
|
+
const container = resolveContainer(scrollContainer, root) ?? containerNode;
|
|
383
|
+
if (!root || !container)
|
|
384
|
+
return undefined;
|
|
385
|
+
const view = root.ownerDocument.defaultView;
|
|
386
|
+
let frame = 0;
|
|
387
|
+
const check = () => {
|
|
388
|
+
frame = 0;
|
|
389
|
+
const latest = propsRef.current;
|
|
390
|
+
const activeDirections = normalizeDirections(latest.directions);
|
|
391
|
+
const loadingDirections = activeDirections.filter(direction => currentState(direction).status === 'loading');
|
|
392
|
+
const inlineSentinel = loadingDirections.includes('top')
|
|
393
|
+
? sentinelRefs.current.top
|
|
394
|
+
: loadingDirections.includes('bottom') ? sentinelRefs.current.bottom : null;
|
|
395
|
+
const blockSentinel = loadingDirections.includes('left')
|
|
396
|
+
? sentinelRefs.current.left
|
|
397
|
+
: loadingDirections.includes('right') ? sentinelRefs.current.right : null;
|
|
398
|
+
const viewport = container.getBoundingClientRect();
|
|
399
|
+
const scaleX = viewport.width > 0 && container.offsetWidth > 0 ? viewport.width / container.offsetWidth : 1;
|
|
400
|
+
const scaleY = viewport.height > 0 && container.offsetHeight > 0 ? viewport.height / container.offsetHeight : 1;
|
|
401
|
+
const viewportLeft = viewport.left + container.clientLeft * scaleX;
|
|
402
|
+
const viewportTop = viewport.top + container.clientTop * scaleY;
|
|
403
|
+
const viewportRight = viewportLeft + container.clientWidth * scaleX;
|
|
404
|
+
const viewportBottom = viewportTop + container.clientHeight * scaleY;
|
|
405
|
+
const inlineOffset = inlineSentinel
|
|
406
|
+
? localScrollDelta(container, 'left', viewportLeft - inlineSentinel.getBoundingClientRect().left)
|
|
407
|
+
: 0;
|
|
408
|
+
const blockOffset = blockSentinel
|
|
409
|
+
? localScrollDelta(container, 'top', viewportTop - blockSentinel.getBoundingClientRect().top)
|
|
410
|
+
: 0;
|
|
411
|
+
root.style.setProperty('--inkra-infinite-scroll-indicator-inline-offset', `${inlineOffset}px`);
|
|
412
|
+
root.style.setProperty('--inkra-infinite-scroll-indicator-block-offset', `${blockOffset}px`);
|
|
413
|
+
root.style.setProperty('--inkra-infinite-scroll-indicator-viewport-inline-size', `${container.clientWidth}px`);
|
|
414
|
+
root.style.setProperty('--inkra-infinite-scroll-indicator-viewport-block-size', `${container.clientHeight}px`);
|
|
415
|
+
for (const direction of activeDirections) {
|
|
416
|
+
const threshold = finiteDistance(directionalValue(latest.triggerDistance, direction, 160), 160);
|
|
417
|
+
const distance = physicalBoundaryDistance(container, sentinelRefs.current[direction], direction, root);
|
|
418
|
+
const status = currentState(direction).status;
|
|
419
|
+
const request = requests.current.get(direction);
|
|
420
|
+
if (status === 'loading' && request && !request.revealed) {
|
|
421
|
+
request.revealed = true;
|
|
422
|
+
const currentPosition = direction === 'top' || direction === 'bottom' ? container.scrollTop : container.scrollLeft;
|
|
423
|
+
if (Math.abs(currentPosition - request.revealPosition) <= threshold + 4) {
|
|
424
|
+
const rect = sentinelRefs.current[direction]?.getBoundingClientRect();
|
|
425
|
+
const reveal = !rect ? 0
|
|
426
|
+
: direction === 'top' ? Math.min(0, rect.top - viewportTop)
|
|
427
|
+
: direction === 'bottom' ? Math.max(0, rect.bottom - viewportBottom)
|
|
428
|
+
: direction === 'left' ? Math.min(0, rect.left - viewportLeft)
|
|
429
|
+
: Math.max(0, rect.right - viewportRight);
|
|
430
|
+
if (Math.abs(reveal) > 0.5) {
|
|
431
|
+
const revealDelta = localScrollDelta(container, direction === 'top' || direction === 'bottom' ? 'top' : 'left', reveal);
|
|
432
|
+
container.scrollBy(direction === 'top' || direction === 'bottom'
|
|
433
|
+
? { behavior: 'auto', top: revealDelta }
|
|
434
|
+
: { behavior: 'auto', left: revealDelta });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (distance > threshold + 4) {
|
|
439
|
+
lastBoundary.current.delete(direction);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (distance <= threshold && status === 'idle' && !requests.current.has(direction)
|
|
443
|
+
&& directionalValue(latest.automatic, direction, true)
|
|
444
|
+
&& !directionalValue(latest.disabled, direction, false)
|
|
445
|
+
&& directionalValue(latest.hasMore, direction, true)) {
|
|
446
|
+
const signature = boundarySignature(container, root);
|
|
447
|
+
if (lastBoundary.current.get(direction) !== signature) {
|
|
448
|
+
lastBoundary.current.set(direction, signature);
|
|
449
|
+
runLoad(direction, 'boundary');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
const schedule = () => {
|
|
455
|
+
if (!frame && view)
|
|
456
|
+
frame = view.requestAnimationFrame(check);
|
|
457
|
+
else if (!view)
|
|
458
|
+
check();
|
|
459
|
+
};
|
|
460
|
+
scheduleBoundaryRef.current = schedule;
|
|
461
|
+
const handleScroll = (event) => {
|
|
462
|
+
schedule();
|
|
463
|
+
};
|
|
464
|
+
container.addEventListener('scroll', handleScroll, { passive: true });
|
|
465
|
+
const resize = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
|
|
466
|
+
resize?.observe(container);
|
|
467
|
+
resize?.observe(root);
|
|
468
|
+
const mutation = typeof MutationObserver === 'undefined' ? null : new MutationObserver(schedule);
|
|
469
|
+
mutation?.observe(contentRef.current ?? root, { childList: true, subtree: true });
|
|
470
|
+
schedule();
|
|
471
|
+
return () => {
|
|
472
|
+
if (frame && view)
|
|
473
|
+
view.cancelAnimationFrame(frame);
|
|
474
|
+
container.removeEventListener('scroll', handleScroll);
|
|
475
|
+
resize?.disconnect();
|
|
476
|
+
mutation?.disconnect();
|
|
477
|
+
if (scheduleBoundaryRef.current === schedule)
|
|
478
|
+
scheduleBoundaryRef.current = () => { };
|
|
479
|
+
};
|
|
480
|
+
}, [containerNode, scrollContainer, directionsProp, automatic, disabled, hasMore, triggerDistance, currentState, runLoad]);
|
|
481
|
+
useBrowserLayoutEffect(() => {
|
|
482
|
+
for (const direction of infiniteScrollDirections) {
|
|
483
|
+
if (!directions.includes(direction) || directionalValue(disabled, direction, false)) {
|
|
484
|
+
cancelDirection(direction, directions.includes(direction) ? 'disabled' : 'direction-removed');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
gestureRef.current?.cancel();
|
|
488
|
+
}, [directions, disabled, gesture, gestureThreshold, scrollContainer, cancelDirection]);
|
|
489
|
+
useBrowserLayoutEffect(() => () => {
|
|
490
|
+
gestureRef.current?.dispose();
|
|
491
|
+
for (const request of requests.current.values()) {
|
|
492
|
+
request.controller.abort();
|
|
493
|
+
request.anchor.abort();
|
|
494
|
+
request.committed?.();
|
|
495
|
+
}
|
|
496
|
+
requests.current.clear();
|
|
497
|
+
}, []);
|
|
498
|
+
const labelFor = (direction, status) => labels?.[status] ?? copy[direction][status];
|
|
499
|
+
const renderDefaultIndicator = (direction, status, progress, error) => {
|
|
500
|
+
const label = labelFor(direction, status);
|
|
501
|
+
if (status === 'loading')
|
|
502
|
+
return _jsxs("div", { className: "inkra-infinite-scroll__loading", children: [_jsx(Progress, { "aria-label": label, diameter: 32, shape: "circular", size: "sm", thickness: 3, value: null }), _jsx(PencilText, { "aria-hidden": "true", as: "span", className: "inkra-infinite-scroll__loading-label", children: label })] });
|
|
503
|
+
if (status === 'pulling' || status === 'ready') {
|
|
504
|
+
return _jsxs("div", { className: "inkra-infinite-scroll__loading", children: [_jsx(Progress, { "aria-label": label, diameter: 24, shape: "circular", thickness: 2, max: 100, size: "sm", value: progress * 100 }), _jsx(PencilText, { "aria-hidden": "true", className: "inkra-infinite-scroll__loading-label", children: label })] });
|
|
505
|
+
}
|
|
506
|
+
if (status === 'error')
|
|
507
|
+
return _jsxs(_Fragment, { children: [typeof errorContent === 'function' ? errorContent(direction, error) : errorContent ?? _jsx(PencilText, { children: label }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => runLoad(direction, 'retry'), children: labels?.retry ?? copy.retry })] });
|
|
508
|
+
if (status === 'end') {
|
|
509
|
+
return typeof endContent === 'function' ? endContent(direction) : endContent ?? _jsx(PencilText, { children: label });
|
|
510
|
+
}
|
|
511
|
+
return _jsx(Button, { "aria-label": label, size: "sm", variant: "ghost", onClick: () => runLoad(direction, 'button'), children: labels?.loadMore ?? label });
|
|
512
|
+
};
|
|
513
|
+
const indicator = (direction) => {
|
|
514
|
+
const active = directions.includes(direction);
|
|
515
|
+
const resolved = state?.[direction] ?? internalState[direction];
|
|
516
|
+
const directionHasMore = directionalValue(hasMore, direction, true);
|
|
517
|
+
const status = directionHasMore ? resolved.status : 'end';
|
|
518
|
+
const visible = active && indicatorVisible(status, directionalValue(automatic, direction, true), directionalValue(gesture, direction, true));
|
|
519
|
+
const progress = pull.direction === direction ? pull.progress : status === 'ready' ? 1 : 0;
|
|
520
|
+
const label = labelFor(direction, status);
|
|
521
|
+
const content = visible
|
|
522
|
+
? renderIndicator?.({
|
|
523
|
+
direction,
|
|
524
|
+
status,
|
|
525
|
+
progress,
|
|
526
|
+
error: resolved.error,
|
|
527
|
+
label,
|
|
528
|
+
retry: () => runLoad(direction, 'retry'),
|
|
529
|
+
load: () => runLoad(direction, 'button'),
|
|
530
|
+
}) ?? renderDefaultIndicator(direction, status, progress, resolved.error)
|
|
531
|
+
: null;
|
|
532
|
+
return _jsx("div", { "aria-hidden": !visible || undefined, "aria-live": visible ? 'polite' : undefined, className: "inkra-infinite-scroll__indicator", "data-active": active || undefined, "data-direction": direction, "data-status": active ? status : undefined, "data-pulling": pull.direction === direction || undefined, "data-visible": visible || undefined, ref: node => { sentinelRefs.current[direction] = node; }, children: visible && _jsx("div", { className: "inkra-infinite-scroll__indicator-surface", children: _jsx("div", { className: "inkra-infinite-scroll__indicator-content", children: content }) }) });
|
|
533
|
+
};
|
|
534
|
+
const resolvedStyle = {
|
|
535
|
+
...style,
|
|
536
|
+
'--inkra-infinite-scroll-pull-distance': `${pull.distance}px`,
|
|
537
|
+
};
|
|
538
|
+
return _jsxs("div", { ...domProps, className: ['inkra-infinite-scroll', className].filter(Boolean).join(' '), "data-pull-direction": pull.direction ?? undefined, onTouchStart: (event) => {
|
|
539
|
+
onTouchStart?.(event);
|
|
540
|
+
if (!event.defaultPrevented)
|
|
541
|
+
gestureRef.current?.start(event.nativeEvent);
|
|
542
|
+
}, ref: setRoot, style: resolvedStyle, children: [indicator('top'), _jsxs("div", { className: "inkra-infinite-scroll__body", children: [indicator('left'), _jsx("div", { className: "inkra-infinite-scroll__content", ref: contentRef, children: children }), indicator('right')] }), indicator('bottom')] });
|
|
543
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type InfiniteScrollDirection = 'top' | 'bottom' | 'left' | 'right';
|
|
2
|
+
export type InfiniteScrollAxis = 'horizontal' | 'vertical';
|
|
3
|
+
export type InfiniteScrollStatus = 'idle' | 'pulling' | 'ready' | 'loading' | 'error' | 'end';
|
|
4
|
+
export type InfiniteScrollLoadSource = 'boundary' | 'gesture' | 'button' | 'retry';
|
|
5
|
+
export declare const infiniteScrollDirections: readonly ["top", "bottom", "left", "right"];
|
|
6
|
+
export declare function completeDirectionalRecord<T>(value: Partial<Record<InfiniteScrollDirection, T>> | undefined, fallback: (direction: InfiniteScrollDirection) => T): Record<InfiniteScrollDirection, T>;
|
|
7
|
+
export declare function normalizeDirections(value: InfiniteScrollDirection | readonly InfiniteScrollDirection[] | undefined): InfiniteScrollDirection[];
|
|
8
|
+
export declare function directionAxis(direction: InfiniteScrollDirection): InfiniteScrollAxis;
|
|
9
|
+
export declare function directionSign(direction: InfiniteScrollDirection): -1 | 1;
|
|
10
|
+
export declare function directionalValue<T>(value: T | Partial<Record<InfiniteScrollDirection, T>> | undefined, direction: InfiniteScrollDirection, fallback: T): T;
|
|
11
|
+
export declare function finiteDistance(value: number | undefined, fallback: number, minimum?: number): number;
|
|
12
|
+
export interface ScrollMetrics {
|
|
13
|
+
scrollTop: number;
|
|
14
|
+
scrollLeft: number;
|
|
15
|
+
scrollHeight: number;
|
|
16
|
+
scrollWidth: number;
|
|
17
|
+
clientHeight: number;
|
|
18
|
+
clientWidth: number;
|
|
19
|
+
}
|
|
20
|
+
export type RtlScrollType = 'default' | 'negative' | 'reverse';
|
|
21
|
+
export declare function boundaryDistance(metrics: ScrollMetrics, direction: InfiniteScrollDirection): number;
|
|
22
|
+
export declare function physicalHorizontalBoundaryDistance(metrics: ScrollMetrics, direction: 'left' | 'right', rtl?: boolean, rtlType?: RtlScrollType): number;
|
|
23
|
+
export declare function dampPullDistance(distance: number, threshold: number): number;
|
|
24
|
+
export declare function pullProgress(distance: number, threshold: number): number;
|
|
25
|
+
export declare function gestureStatusEnabled(status: InfiniteScrollStatus): boolean;
|
|
26
|
+
export declare function indicatorVisible(status: InfiniteScrollStatus, automatic: boolean, gesture?: boolean): boolean;
|
|
27
|
+
export declare function statusAfterCancellation(status: InfiniteScrollStatus): InfiniteScrollStatus;
|
|
28
|
+
export declare function localScrollDelta(container: Pick<HTMLElement, 'clientHeight' | 'clientWidth' | 'getBoundingClientRect'>, direction: 'top' | 'left', physicalDelta: number): number;
|
|
29
|
+
export declare function boundarySignature(container: HTMLElement, root: HTMLElement): string;
|
|
30
|
+
export declare function isPageScrollContainer(container: HTMLElement): boolean;
|
|
31
|
+
export declare function physicalBoundaryDistance(container: HTMLElement, sentinel: HTMLElement | null, direction: InfiniteScrollDirection, root?: HTMLElement | null): number;
|
|
32
|
+
export declare function isEditableOrInteractiveTarget(target: Element | null): boolean;
|
|
33
|
+
export declare function hasNestedScrollOwner(target: HTMLElement, root: HTMLElement, container: HTMLElement, axis: InfiniteScrollAxis): boolean;
|