panelui-native 0.65.0 → 0.68.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 +21 -1
- package/lib/module/components/area-chart/index.js +18 -1
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +18 -1
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/candlestick-chart/index.js +18 -1
- package/lib/module/components/candlestick-chart/index.js.map +1 -1
- package/lib/module/components/carousel/carousel-lifecycle.js +51 -0
- package/lib/module/components/carousel/carousel-lifecycle.js.map +1 -0
- package/lib/module/components/carousel/index.js +61 -24
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/context-menu/context-menu-invocation.js +23 -0
- package/lib/module/components/context-menu/context-menu-invocation.js.map +1 -0
- package/lib/module/components/context-menu/index.js +78 -9
- package/lib/module/components/context-menu/index.js.map +1 -1
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/flow/flow-accessibility.js +18 -2
- package/lib/module/components/flow/flow-accessibility.js.map +1 -1
- package/lib/module/components/flow/flow-identifiers.js +81 -0
- package/lib/module/components/flow/flow-identifiers.js.map +1 -0
- package/lib/module/components/flow/index.js +12 -18
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/form/index.js +7 -5
- package/lib/module/components/form/index.js.map +1 -1
- package/lib/module/components/form/typed-form.js +11 -0
- package/lib/module/components/form/typed-form.js.map +1 -0
- package/lib/module/components/form/use-form.js +4 -0
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +27 -1
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +18 -1
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/message-scroller/index.js +187 -17
- package/lib/module/components/message-scroller/index.js.map +1 -1
- package/lib/module/components/message-scroller/message-scroller-math.js +43 -0
- package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -0
- package/lib/module/components/meter/index.js +250 -0
- package/lib/module/components/meter/index.js.map +1 -0
- package/lib/module/components/meter/meter-scale.js +88 -0
- package/lib/module/components/meter/meter-scale.js.map +1 -0
- package/lib/module/components/planner/index.js +714 -0
- package/lib/module/components/planner/index.js.map +1 -0
- package/lib/module/components/planner/planner-entries.js +124 -0
- package/lib/module/components/planner/planner-entries.js.map +1 -0
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/radar-chart/index.js +18 -1
- package/lib/module/components/radar-chart/index.js.map +1 -1
- package/lib/module/components/scatter-chart/index.js +18 -1
- package/lib/module/components/scatter-chart/index.js.map +1 -1
- package/lib/module/components/selection-mode/index.js +26 -31
- package/lib/module/components/selection-mode/index.js.map +1 -1
- package/lib/module/components/selection-mode/selection-mode-contracts.js +48 -0
- package/lib/module/components/selection-mode/selection-mode-contracts.js.map +1 -0
- package/lib/module/components/signature/index.js +62 -20
- package/lib/module/components/signature/index.js.map +1 -1
- package/lib/module/components/signature/signature-accessibility.js +58 -0
- package/lib/module/components/signature/signature-accessibility.js.map +1 -0
- package/lib/module/components/skeleton/index.js +45 -7
- package/lib/module/components/skeleton/index.js.map +1 -1
- package/lib/module/components/slider/index.js +22 -18
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/spinner/index.js +63 -14
- package/lib/module/components/spinner/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +46 -21
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/time-picker/ruler-window.js +25 -0
- package/lib/module/components/time-picker/ruler-window.js.map +1 -0
- package/lib/module/hooks/breakpoint-contract.js +29 -0
- package/lib/module/hooks/breakpoint-contract.js.map +1 -0
- package/lib/module/hooks/index.js +1 -1
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-breakpoint.js +46 -1
- package/lib/module/hooks/use-breakpoint.js.map +1 -1
- package/lib/module/index.js +3 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/chart-accessibility.js +68 -0
- package/lib/module/primitives/chart-accessibility.js.map +1 -0
- package/lib/module/primitives/scrim.js +94 -3
- package/lib/module/primitives/scrim.js.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +3 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/candlestick-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts +23 -0
- package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/carousel/index.d.ts +5 -2
- package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
- package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts +20 -0
- package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts.map +1 -0
- package/lib/typescript/src/components/context-menu/index.d.ts +18 -8
- package/lib/typescript/src/components/context-menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/dialog/index.d.ts +3 -0
- package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/components/drawer/index.d.ts +3 -0
- package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts +8 -0
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/flow-identifiers.d.ts +24 -0
- package/lib/typescript/src/components/flow/flow-identifiers.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +6 -5
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/index.d.ts +18 -5
- package/lib/typescript/src/components/form/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/typed-form.d.ts +7 -0
- package/lib/typescript/src/components/form/typed-form.d.ts.map +1 -0
- package/lib/typescript/src/components/form/use-form.d.ts +4 -0
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/index.d.ts +30 -2
- package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts +18 -0
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -0
- package/lib/typescript/src/components/meter/index.d.ts +270 -0
- package/lib/typescript/src/components/meter/index.d.ts.map +1 -0
- package/lib/typescript/src/components/meter/meter-scale.d.ts +57 -0
- package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/index.d.ts +274 -0
- package/lib/typescript/src/components/planner/index.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-entries.d.ts +75 -0
- package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -0
- package/lib/typescript/src/components/popover/index.d.ts +3 -0
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radar-chart/index.d.ts +4 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/scatter-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/index.d.ts +6 -4
- package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts +18 -0
- package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts.map +1 -0
- package/lib/typescript/src/components/signature/index.d.ts +6 -0
- package/lib/typescript/src/components/signature/index.d.ts.map +1 -1
- package/lib/typescript/src/components/signature/signature-accessibility.d.ts +27 -0
- package/lib/typescript/src/components/signature/signature-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/skeleton/index.d.ts +25 -2
- package/lib/typescript/src/components/skeleton/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/spinner/index.d.ts +25 -2
- package/lib/typescript/src/components/spinner/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts +5 -6
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/ruler-window.d.ts +12 -0
- package/lib/typescript/src/components/time-picker/ruler-window.d.ts.map +1 -0
- package/lib/typescript/src/hooks/breakpoint-contract.d.ts +6 -0
- package/lib/typescript/src/hooks/breakpoint-contract.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +1 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-breakpoint.d.ts +22 -0
- package/lib/typescript/src/hooks/use-breakpoint.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/chart-accessibility.d.ts +33 -0
- package/lib/typescript/src/primitives/chart-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/primitives/scrim.d.ts +5 -3
- package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
- package/lib/typescript/types/form-api.type-test.d.ts +2 -0
- package/lib/typescript/types/form-api.type-test.d.ts.map +1 -0
- package/package.json +34 -5
- package/src/components/area-chart/index.tsx +27 -2
- package/src/components/bar-chart/index.tsx +27 -2
- package/src/components/bottom-sheet/index.tsx +3 -0
- package/src/components/candlestick-chart/index.tsx +30 -2
- package/src/components/carousel/carousel-lifecycle.ts +74 -0
- package/src/components/carousel/index.tsx +80 -25
- package/src/components/context-menu/context-menu-invocation.ts +44 -0
- package/src/components/context-menu/index.tsx +113 -13
- package/src/components/dialog/index.tsx +3 -0
- package/src/components/drawer/index.tsx +3 -0
- package/src/components/flow/flow-accessibility.ts +16 -2
- package/src/components/flow/flow-identifiers.ts +98 -0
- package/src/components/flow/index.tsx +29 -19
- package/src/components/form/index.tsx +32 -5
- package/src/components/form/typed-form.ts +13 -0
- package/src/components/form/use-form.ts +9 -0
- package/src/components/heatmap-chart/index.tsx +49 -3
- package/src/components/line-chart/index.tsx +27 -2
- package/src/components/message-scroller/index.tsx +273 -13
- package/src/components/message-scroller/message-scroller-math.ts +62 -0
- package/src/components/meter/index.tsx +318 -0
- package/src/components/meter/meter-scale.ts +109 -0
- package/src/components/planner/index.tsx +886 -0
- package/src/components/planner/planner-entries.ts +146 -0
- package/src/components/popover/index.tsx +3 -0
- package/src/components/radar-chart/index.tsx +24 -0
- package/src/components/scatter-chart/index.tsx +27 -2
- package/src/components/selection-mode/index.tsx +37 -30
- package/src/components/selection-mode/selection-mode-contracts.ts +53 -0
- package/src/components/signature/index.tsx +107 -20
- package/src/components/signature/signature-accessibility.ts +67 -0
- package/src/components/skeleton/index.tsx +59 -5
- package/src/components/slider/index.tsx +28 -18
- package/src/components/spinner/index.tsx +80 -13
- package/src/components/time-picker/index.tsx +57 -29
- package/src/components/time-picker/ruler-window.ts +24 -0
- package/src/hooks/breakpoint-contract.ts +31 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/use-breakpoint.ts +61 -10
- package/src/index.ts +32 -0
- package/src/primitives/chart-accessibility.ts +102 -0
- package/src/primitives/scrim.tsx +127 -7
|
@@ -39,9 +39,18 @@ import {
|
|
|
39
39
|
useMemo,
|
|
40
40
|
useRef,
|
|
41
41
|
useState,
|
|
42
|
+
type ReactElement,
|
|
42
43
|
type ReactNode,
|
|
43
44
|
} from 'react';
|
|
44
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
FlatList,
|
|
47
|
+
View,
|
|
48
|
+
type FlatListProps,
|
|
49
|
+
type LayoutChangeEvent,
|
|
50
|
+
type ListRenderItemInfo,
|
|
51
|
+
type ViewProps,
|
|
52
|
+
type ViewToken,
|
|
53
|
+
} from 'react-native';
|
|
45
54
|
import Animated, {
|
|
46
55
|
runOnJS,
|
|
47
56
|
useAnimatedRef,
|
|
@@ -57,9 +66,15 @@ import { ChevronDownIcon } from '../../icons';
|
|
|
57
66
|
import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
58
67
|
import { cn } from '../../utils/cn';
|
|
59
68
|
import { textChildren } from '../../primitives/text';
|
|
69
|
+
import {
|
|
70
|
+
distanceFromMessageScrollerTarget,
|
|
71
|
+
initialMessageScrollerIndex,
|
|
72
|
+
isMessageScrollerTargetVisible,
|
|
73
|
+
messageScrollerAnchorAt,
|
|
74
|
+
messageScrollerIndex,
|
|
75
|
+
MESSAGE_SCROLLER_EDGE_THRESHOLD,
|
|
76
|
+
} from './message-scroller-math';
|
|
60
77
|
|
|
61
|
-
/** Within this many pixels of the bottom still counts as being at the bottom. */
|
|
62
|
-
const END_THRESHOLD = 24;
|
|
63
78
|
/**
|
|
64
79
|
* How much of the previous turn is left showing above an anchored one. Scrolling
|
|
65
80
|
* a turn flush to the top reads as content having been cut off; a sliver of the
|
|
@@ -69,10 +84,20 @@ const ANCHOR_PEEK = 24;
|
|
|
69
84
|
|
|
70
85
|
export type MessageScrollerPosition = 'start' | 'end' | 'last-anchor';
|
|
71
86
|
|
|
87
|
+
interface MessageScrollerDriver {
|
|
88
|
+
scrollToEnd: (animated: boolean) => void;
|
|
89
|
+
scrollToStart: (animated: boolean) => void;
|
|
90
|
+
scrollToMessage: (id: string, animated: boolean) => boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
interface MessageScrollerContextValue {
|
|
73
94
|
scrollRef: ReturnType<typeof useAnimatedRef<Animated.ScrollView>>;
|
|
74
|
-
|
|
95
|
+
registerDriver: (driver: MessageScrollerDriver | null) => void;
|
|
96
|
+
/** 1 while the reader is at the live edge, 0 otherwise. Drives follow state. */
|
|
75
97
|
atEnd: ReturnType<typeof useSharedValue<number>>;
|
|
98
|
+
/** Live distance from each edge, used by target-specific jump controls. */
|
|
99
|
+
distanceFromStart: ReturnType<typeof useSharedValue<number>>;
|
|
100
|
+
distanceFromEnd: ReturnType<typeof useSharedValue<number>>;
|
|
76
101
|
atEndJS: boolean;
|
|
77
102
|
setAtEndJS: (atEnd: boolean) => void;
|
|
78
103
|
autoScroll: boolean;
|
|
@@ -150,12 +175,21 @@ function MessageScrollerRoot({
|
|
|
150
175
|
}: MessageScrollerProps) {
|
|
151
176
|
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
|
152
177
|
const atEnd = useSharedValue(1);
|
|
178
|
+
const awayFromEdge = MESSAGE_SCROLLER_EDGE_THRESHOLD + 1;
|
|
179
|
+
const distanceFromStart = useSharedValue(
|
|
180
|
+
defaultScrollPosition === 'start' ? 0 : awayFromEdge
|
|
181
|
+
);
|
|
182
|
+
const distanceFromEnd = useSharedValue(defaultScrollPosition === 'end' ? 0 : awayFromEdge);
|
|
153
183
|
const [atEndJS, setAtEndJS] = useState(true);
|
|
154
184
|
const [currentAnchorId, setCurrentAnchorId] = useState<string | null>(null);
|
|
155
185
|
|
|
156
186
|
const itemY = useRef(new Map<string, number>());
|
|
157
187
|
const anchors = useRef<string[]>([]);
|
|
158
188
|
const following = useRef(autoScroll);
|
|
189
|
+
const driver = useRef<MessageScrollerDriver | null>(null);
|
|
190
|
+
const registerDriver = useCallback((next: MessageScrollerDriver | null) => {
|
|
191
|
+
driver.current = next;
|
|
192
|
+
}, []);
|
|
159
193
|
|
|
160
194
|
const registerItem = useCallback((id: string, y: number, anchor: boolean) => {
|
|
161
195
|
itemY.current.set(id, y);
|
|
@@ -172,7 +206,8 @@ function MessageScrollerRoot({
|
|
|
172
206
|
const scrollToEnd = useCallback(
|
|
173
207
|
(animated = true) => {
|
|
174
208
|
following.current = true;
|
|
175
|
-
|
|
209
|
+
if (driver.current) driver.current.scrollToEnd(animated);
|
|
210
|
+
else scrollRef.current?.scrollToEnd({ animated });
|
|
176
211
|
},
|
|
177
212
|
[scrollRef]
|
|
178
213
|
);
|
|
@@ -180,17 +215,23 @@ function MessageScrollerRoot({
|
|
|
180
215
|
const scrollToStart = useCallback(
|
|
181
216
|
(animated = true) => {
|
|
182
217
|
following.current = false;
|
|
183
|
-
|
|
218
|
+
if (driver.current) driver.current.scrollToStart(animated);
|
|
219
|
+
else scrollRef.current?.scrollTo({ y: 0, animated });
|
|
184
220
|
},
|
|
185
221
|
[scrollRef]
|
|
186
222
|
);
|
|
187
223
|
|
|
188
224
|
const scrollToMessage = useCallback(
|
|
189
225
|
(id: string, animated = true) => {
|
|
226
|
+
if (driver.current) {
|
|
227
|
+
if (driver.current.scrollToMessage(id, animated)) following.current = false;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
190
230
|
const y = itemY.current.get(id);
|
|
191
|
-
if (y
|
|
192
|
-
|
|
193
|
-
|
|
231
|
+
if (y !== undefined) {
|
|
232
|
+
following.current = false;
|
|
233
|
+
scrollRef.current?.scrollTo({ y: Math.max(0, y - ANCHOR_PEEK), animated });
|
|
234
|
+
}
|
|
194
235
|
},
|
|
195
236
|
[scrollRef]
|
|
196
237
|
);
|
|
@@ -198,7 +239,10 @@ function MessageScrollerRoot({
|
|
|
198
239
|
const context = useMemo<MessageScrollerContextValue>(
|
|
199
240
|
() => ({
|
|
200
241
|
scrollRef,
|
|
242
|
+
registerDriver,
|
|
201
243
|
atEnd,
|
|
244
|
+
distanceFromStart,
|
|
245
|
+
distanceFromEnd,
|
|
202
246
|
atEndJS,
|
|
203
247
|
setAtEndJS,
|
|
204
248
|
autoScroll,
|
|
@@ -217,7 +261,10 @@ function MessageScrollerRoot({
|
|
|
217
261
|
}),
|
|
218
262
|
[
|
|
219
263
|
scrollRef,
|
|
264
|
+
registerDriver,
|
|
220
265
|
atEnd,
|
|
266
|
+
distanceFromStart,
|
|
267
|
+
distanceFromEnd,
|
|
221
268
|
atEndJS,
|
|
222
269
|
autoScroll,
|
|
223
270
|
preserveScrollOnPrepend,
|
|
@@ -268,6 +315,8 @@ function MessageScrollerViewport({
|
|
|
268
315
|
const {
|
|
269
316
|
scrollRef,
|
|
270
317
|
atEnd,
|
|
318
|
+
distanceFromStart,
|
|
319
|
+
distanceFromEnd,
|
|
271
320
|
setAtEndJS,
|
|
272
321
|
autoScroll,
|
|
273
322
|
preserveScrollOnPrepend,
|
|
@@ -310,8 +359,19 @@ function MessageScrollerViewport({
|
|
|
310
359
|
const { contentOffset, contentSize, layoutMeasurement } = event;
|
|
311
360
|
offsetY.value = contentOffset.y;
|
|
312
361
|
|
|
313
|
-
|
|
314
|
-
|
|
362
|
+
distanceFromStart.value = distanceFromMessageScrollerTarget(
|
|
363
|
+
'start',
|
|
364
|
+
contentOffset.y,
|
|
365
|
+
contentSize.height,
|
|
366
|
+
layoutMeasurement.height
|
|
367
|
+
);
|
|
368
|
+
distanceFromEnd.value = distanceFromMessageScrollerTarget(
|
|
369
|
+
'end',
|
|
370
|
+
contentOffset.y,
|
|
371
|
+
contentSize.height,
|
|
372
|
+
layoutMeasurement.height
|
|
373
|
+
);
|
|
374
|
+
const next = isMessageScrollerTargetVisible(distanceFromEnd.value) ? 0 : 1;
|
|
315
375
|
if (next !== atEnd.value) {
|
|
316
376
|
atEnd.value = next;
|
|
317
377
|
runOnJS(publishAtEnd)(next === 1);
|
|
@@ -411,6 +471,201 @@ function MessageScrollerViewport({
|
|
|
411
471
|
}
|
|
412
472
|
MessageScrollerViewport.displayName = 'MessageScroller.Viewport';
|
|
413
473
|
|
|
474
|
+
export interface MessageScrollerListItem {
|
|
475
|
+
/** Stable id used by `scrollToMessage` and as the default React key. */
|
|
476
|
+
messageId: string;
|
|
477
|
+
/** Marks the start of a turn for `last-anchor` and visibility reporting. */
|
|
478
|
+
scrollAnchor?: boolean;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export interface MessageScrollerListProps<T extends MessageScrollerListItem = MessageScrollerListItem>
|
|
482
|
+
extends Omit<
|
|
483
|
+
FlatListProps<T>,
|
|
484
|
+
| 'data'
|
|
485
|
+
| 'renderItem'
|
|
486
|
+
| 'keyExtractor'
|
|
487
|
+
| 'onScroll'
|
|
488
|
+
| 'onContentSizeChange'
|
|
489
|
+
| 'onViewableItemsChanged'
|
|
490
|
+
| 'maintainVisibleContentPosition'
|
|
491
|
+
| 'onScrollToIndexFailed'
|
|
492
|
+
| 'onScrollEndDrag'
|
|
493
|
+
| 'onMomentumScrollEnd'
|
|
494
|
+
| 'CellRendererComponent'
|
|
495
|
+
> {
|
|
496
|
+
className?: string;
|
|
497
|
+
/** Classes on the virtualized list's padded transcript column. */
|
|
498
|
+
contentContainerClassName?: string;
|
|
499
|
+
/**
|
|
500
|
+
* The complete transcript. Rows outside the native render window stay
|
|
501
|
+
* unmounted; each item therefore carries its stable navigation metadata.
|
|
502
|
+
*/
|
|
503
|
+
data: readonly T[];
|
|
504
|
+
/** Draw one turn from the native list window. */
|
|
505
|
+
renderItem: (info: ListRenderItemInfo<T>) => ReactElement | null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* The real virtualized transcript path. Only its render window mounts; native
|
|
510
|
+
* visible-content maintenance keeps prepends still without measuring offscreen
|
|
511
|
+
* rows in JavaScript.
|
|
512
|
+
*/
|
|
513
|
+
function MessageScrollerList<T extends MessageScrollerListItem>({
|
|
514
|
+
className,
|
|
515
|
+
contentContainerClassName,
|
|
516
|
+
data,
|
|
517
|
+
renderItem,
|
|
518
|
+
initialNumToRender = 12,
|
|
519
|
+
maxToRenderPerBatch = 8,
|
|
520
|
+
windowSize = 7,
|
|
521
|
+
...props
|
|
522
|
+
}: MessageScrollerListProps<T>) {
|
|
523
|
+
const {
|
|
524
|
+
atEnd,
|
|
525
|
+
distanceFromStart,
|
|
526
|
+
distanceFromEnd,
|
|
527
|
+
setAtEndJS,
|
|
528
|
+
autoScroll,
|
|
529
|
+
preserveScrollOnPrepend,
|
|
530
|
+
defaultScrollPosition,
|
|
531
|
+
following,
|
|
532
|
+
registerDriver,
|
|
533
|
+
setCurrentAnchorId,
|
|
534
|
+
} = useScroller('MessageScroller.List');
|
|
535
|
+
const listRef = useRef<FlatList<T>>(null);
|
|
536
|
+
const items = useRef(data);
|
|
537
|
+
items.current = data;
|
|
538
|
+
const opened = useRef(false);
|
|
539
|
+
const firstVisibleIndex = useRef<number | null>(null);
|
|
540
|
+
|
|
541
|
+
const publishAtEnd = useCallback(
|
|
542
|
+
(next: boolean) => {
|
|
543
|
+
following.current = autoScroll && next;
|
|
544
|
+
setAtEndJS(next);
|
|
545
|
+
},
|
|
546
|
+
[autoScroll, following, setAtEndJS]
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
550
|
+
onScroll: (event) => {
|
|
551
|
+
const { contentOffset, contentSize, layoutMeasurement } = event;
|
|
552
|
+
distanceFromStart.value = distanceFromMessageScrollerTarget(
|
|
553
|
+
'start',
|
|
554
|
+
contentOffset.y,
|
|
555
|
+
contentSize.height,
|
|
556
|
+
layoutMeasurement.height
|
|
557
|
+
);
|
|
558
|
+
distanceFromEnd.value = distanceFromMessageScrollerTarget(
|
|
559
|
+
'end',
|
|
560
|
+
contentOffset.y,
|
|
561
|
+
contentSize.height,
|
|
562
|
+
layoutMeasurement.height
|
|
563
|
+
);
|
|
564
|
+
const next = isMessageScrollerTargetVisible(distanceFromEnd.value) ? 0 : 1;
|
|
565
|
+
if (next !== atEnd.value) {
|
|
566
|
+
atEnd.value = next;
|
|
567
|
+
runOnJS(publishAtEnd)(next === 1);
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const scrollToIndex = useCallback((index: number, animated: boolean) => {
|
|
573
|
+
listRef.current?.scrollToIndex({ index, viewOffset: ANCHOR_PEEK, animated });
|
|
574
|
+
}, []);
|
|
575
|
+
|
|
576
|
+
const setAnchorAtIndex = useCallback(
|
|
577
|
+
(index: number) => setCurrentAnchorId(messageScrollerAnchorAt(items.current, index)),
|
|
578
|
+
[setCurrentAnchorId]
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
useEffect(() => {
|
|
582
|
+
registerDriver({
|
|
583
|
+
scrollToEnd: (animated) => {
|
|
584
|
+
listRef.current?.scrollToEnd({ animated });
|
|
585
|
+
setAnchorAtIndex(items.current.length - 1);
|
|
586
|
+
},
|
|
587
|
+
scrollToStart: (animated) => {
|
|
588
|
+
listRef.current?.scrollToOffset({ offset: 0, animated });
|
|
589
|
+
setAnchorAtIndex(0);
|
|
590
|
+
},
|
|
591
|
+
scrollToMessage: (id, animated) => {
|
|
592
|
+
const index = messageScrollerIndex(items.current, id);
|
|
593
|
+
if (index === undefined) return false;
|
|
594
|
+
scrollToIndex(index, animated);
|
|
595
|
+
setAnchorAtIndex(index);
|
|
596
|
+
return true;
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
return () => registerDriver(null);
|
|
600
|
+
}, [registerDriver, scrollToIndex, setAnchorAtIndex]);
|
|
601
|
+
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
following.current = autoScroll && atEnd.value === 1;
|
|
604
|
+
}, [atEnd, autoScroll, following]);
|
|
605
|
+
|
|
606
|
+
const onContentSizeChange = () => {
|
|
607
|
+
if (!opened.current) {
|
|
608
|
+
opened.current = true;
|
|
609
|
+
const index = initialMessageScrollerIndex(data, defaultScrollPosition);
|
|
610
|
+
const target = index ?? data.length - 1;
|
|
611
|
+
if (index === undefined) listRef.current?.scrollToEnd({ animated: false });
|
|
612
|
+
else if (index > 0) scrollToIndex(index, false);
|
|
613
|
+
setAnchorAtIndex(target);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (following.current) listRef.current?.scrollToEnd({ animated: true });
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const onViewableItemsChanged = useRef(
|
|
620
|
+
({ viewableItems }: { viewableItems: ViewToken<T>[] }) => {
|
|
621
|
+
const first = viewableItems.reduce(
|
|
622
|
+
(best, token) => (token.index !== null && token.index < best ? token.index : best),
|
|
623
|
+
Infinity
|
|
624
|
+
);
|
|
625
|
+
firstVisibleIndex.current = Number.isFinite(first) ? first : null;
|
|
626
|
+
}
|
|
627
|
+
).current;
|
|
628
|
+
|
|
629
|
+
const settleAnchor = () => {
|
|
630
|
+
if (firstVisibleIndex.current !== null) {
|
|
631
|
+
setCurrentAnchorId(messageScrollerAnchorAt(items.current, firstVisibleIndex.current));
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
return (
|
|
636
|
+
<Animated.FlatList
|
|
637
|
+
ref={listRef}
|
|
638
|
+
data={data}
|
|
639
|
+
renderItem={renderItem}
|
|
640
|
+
keyExtractor={(item) => item.messageId}
|
|
641
|
+
accessibilityRole="list"
|
|
642
|
+
accessibilityLabel="Messages"
|
|
643
|
+
accessibilityLiveRegion="polite"
|
|
644
|
+
scrollEventThrottle={16}
|
|
645
|
+
showsVerticalScrollIndicator={false}
|
|
646
|
+
initialNumToRender={initialNumToRender}
|
|
647
|
+
maxToRenderPerBatch={maxToRenderPerBatch}
|
|
648
|
+
windowSize={windowSize}
|
|
649
|
+
maintainVisibleContentPosition={
|
|
650
|
+
preserveScrollOnPrepend ? { minIndexForVisible: 0 } : undefined
|
|
651
|
+
}
|
|
652
|
+
className={cn('flex-1', className)}
|
|
653
|
+
contentContainerClassName={cn('gap-3 p-4 pb-16', contentContainerClassName)}
|
|
654
|
+
{...props}
|
|
655
|
+
onScroll={scrollHandler}
|
|
656
|
+
onContentSizeChange={onContentSizeChange}
|
|
657
|
+
onViewableItemsChanged={onViewableItemsChanged}
|
|
658
|
+
onScrollEndDrag={settleAnchor}
|
|
659
|
+
onMomentumScrollEnd={settleAnchor}
|
|
660
|
+
onScrollToIndexFailed={({ index, averageItemLength }) => {
|
|
661
|
+
listRef.current?.scrollToOffset({ offset: index * averageItemLength, animated: false });
|
|
662
|
+
setTimeout(() => scrollToIndex(index, false), 0);
|
|
663
|
+
}}
|
|
664
|
+
/>
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
MessageScrollerList.displayName = 'MessageScroller.List';
|
|
668
|
+
|
|
414
669
|
/**
|
|
415
670
|
* How far the transcript shifted, measured on the message closest to the top
|
|
416
671
|
* that was there before and is still there.
|
|
@@ -515,12 +770,16 @@ function MessageScrollerButton({
|
|
|
515
770
|
children,
|
|
516
771
|
...props
|
|
517
772
|
}: MessageScrollerButtonProps) {
|
|
518
|
-
const {
|
|
773
|
+
const { distanceFromStart, distanceFromEnd, scrollToEnd, scrollToStart } =
|
|
774
|
+
useScroller('MessageScroller.Button');
|
|
519
775
|
const tint = useCSSVariable('--color-foreground');
|
|
520
776
|
|
|
777
|
+
const targetDistance = target === 'end' ? distanceFromEnd : distanceFromStart;
|
|
521
778
|
// Shown when away from the edge the button points at.
|
|
522
779
|
const shown = useDerivedValue(() =>
|
|
523
|
-
withTiming(
|
|
780
|
+
withTiming(isMessageScrollerTargetVisible(targetDistance.value) ? 1 : 0, {
|
|
781
|
+
duration: 160,
|
|
782
|
+
})
|
|
524
783
|
);
|
|
525
784
|
|
|
526
785
|
const style = useAnimatedStyle(() => ({
|
|
@@ -569,6 +828,7 @@ MessageScrollerButton.displayName = 'MessageScroller.Button';
|
|
|
569
828
|
|
|
570
829
|
export const MessageScroller = Object.assign(MessageScrollerRoot, {
|
|
571
830
|
Viewport: MessageScrollerViewport,
|
|
831
|
+
List: MessageScrollerList,
|
|
572
832
|
Content: MessageScrollerContent,
|
|
573
833
|
Item: MessageScrollerItem,
|
|
574
834
|
Button: MessageScrollerButton,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
type MessageScrollerTarget = 'start' | 'end';
|
|
2
|
+
|
|
3
|
+
export const MESSAGE_SCROLLER_EDGE_THRESHOLD = 24;
|
|
4
|
+
|
|
5
|
+
/** Distance from the viewport to the edge a jump button targets. */
|
|
6
|
+
export function distanceFromMessageScrollerTarget(
|
|
7
|
+
target: MessageScrollerTarget,
|
|
8
|
+
contentOffsetY: number,
|
|
9
|
+
contentHeight: number,
|
|
10
|
+
viewportHeight: number
|
|
11
|
+
): number {
|
|
12
|
+
'worklet';
|
|
13
|
+
|
|
14
|
+
const maximumOffset = Math.max(0, contentHeight - viewportHeight);
|
|
15
|
+
const boundedOffset = Math.min(maximumOffset, Math.max(0, contentOffsetY));
|
|
16
|
+
return target === 'start' ? boundedOffset : maximumOffset - boundedOffset;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Whether the control has enough distance to its target edge to be useful. */
|
|
20
|
+
export function isMessageScrollerTargetVisible(distance: number): boolean {
|
|
21
|
+
'worklet';
|
|
22
|
+
|
|
23
|
+
return distance > MESSAGE_SCROLLER_EDGE_THRESHOLD;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MessageScrollerIndexedItem {
|
|
27
|
+
messageId: string;
|
|
28
|
+
scrollAnchor?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Index used by initial positioning and id-addressed virtualized jumps. */
|
|
32
|
+
export function messageScrollerIndex(
|
|
33
|
+
items: readonly MessageScrollerIndexedItem[],
|
|
34
|
+
id: string
|
|
35
|
+
): number | undefined {
|
|
36
|
+
const index = items.findIndex((item) => item.messageId === id);
|
|
37
|
+
return index < 0 ? undefined : index;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Initial target for a list; `undefined` means use the physical end. */
|
|
41
|
+
export function initialMessageScrollerIndex(
|
|
42
|
+
items: readonly MessageScrollerIndexedItem[],
|
|
43
|
+
position: 'start' | 'end' | 'last-anchor'
|
|
44
|
+
): number | undefined {
|
|
45
|
+
if (position === 'end') return undefined;
|
|
46
|
+
if (position === 'start') return items.length ? 0 : undefined;
|
|
47
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
48
|
+
if (items[index]!.scrollAnchor) return index;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Anchor at or before the first visible virtualized row. */
|
|
54
|
+
export function messageScrollerAnchorAt(
|
|
55
|
+
items: readonly MessageScrollerIndexedItem[],
|
|
56
|
+
firstVisibleIndex: number
|
|
57
|
+
): string | null {
|
|
58
|
+
for (let index = Math.min(firstVisibleIndex, items.length - 1); index >= 0; index -= 1) {
|
|
59
|
+
if (items[index]!.scrollAnchor) return items[index]!.messageId;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|