panelui-native 0.22.5 → 0.25.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 +8 -3
- package/lib/module/components/area-chart/index.js +826 -0
- package/lib/module/components/area-chart/index.js.map +1 -0
- package/lib/module/components/avatar/index.js +1 -1
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +853 -0
- package/lib/module/components/bar-chart/index.js.map +1 -0
- package/lib/module/components/bottom-sheet/index.js +207 -23
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/breadcrumb/index.js +275 -0
- package/lib/module/components/breadcrumb/index.js.map +1 -0
- package/lib/module/components/chip/index.js +2 -2
- package/lib/module/components/direction/index.js +13 -19
- package/lib/module/components/direction/index.js.map +1 -1
- package/lib/module/components/frame/index.js +17 -0
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +28 -8
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/input-group/index.js +2 -2
- package/lib/module/components/input-group/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +4 -146
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/loader/index.js +758 -0
- package/lib/module/components/loader/index.js.map +1 -0
- package/lib/module/components/map/basemap.js +265 -0
- package/lib/module/components/map/basemap.js.map +1 -0
- package/lib/module/components/map/index.js +850 -0
- package/lib/module/components/map/index.js.map +1 -0
- package/lib/module/components/map/maplibre.js +87 -0
- package/lib/module/components/map/maplibre.js.map +1 -0
- package/lib/module/components/message/index.js +8 -7
- package/lib/module/components/message/index.js.map +1 -1
- package/lib/module/components/otp-input/index.js +2 -2
- package/lib/module/components/progress/index.js +8 -2
- package/lib/module/components/progress/index.js.map +1 -1
- package/lib/module/components/ring-chart/index.js +423 -0
- package/lib/module/components/ring-chart/index.js.map +1 -0
- package/lib/module/components/section-rail/index.js +2 -2
- package/lib/module/components/separator/index.js +76 -22
- package/lib/module/components/separator/index.js.map +1 -1
- package/lib/module/components/shimmer/index.js +10 -1
- package/lib/module/components/shimmer/index.js.map +1 -1
- package/lib/module/components/slider/index.js +19 -5
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/steps/index.js +1 -1
- package/lib/module/components/switch/index.js +6 -1
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/typography/index.js +5 -0
- package/lib/module/components/typography/index.js.map +1 -1
- package/lib/module/hooks/use-direction.js +43 -0
- package/lib/module/hooks/use-direction.js.map +1 -0
- package/lib/module/icons/index.js +117 -2
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +8 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/text.js +30 -10
- package/lib/module/primitives/text.js.map +1 -1
- package/lib/module/utils/chart.js +279 -0
- package/lib/module/utils/chart.js.map +1 -0
- package/lib/typescript/src/components/area-chart/index.d.ts +197 -0
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/bar-chart/index.d.ts +228 -0
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +75 -4
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/breadcrumb/index.d.ts +100 -0
- package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -0
- package/lib/typescript/src/components/direction/index.d.ts +11 -10
- package/lib/typescript/src/components/direction/index.d.ts.map +1 -1
- package/lib/typescript/src/components/frame/index.d.ts +17 -0
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +22 -3
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts +1 -0
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/loader/index.d.ts +38 -0
- package/lib/typescript/src/components/loader/index.d.ts.map +1 -0
- package/lib/typescript/src/components/map/basemap.d.ts +76 -0
- package/lib/typescript/src/components/map/basemap.d.ts.map +1 -0
- package/lib/typescript/src/components/map/index.d.ts +367 -0
- package/lib/typescript/src/components/map/index.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +193 -0
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -0
- package/lib/typescript/src/components/message/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
- package/lib/typescript/src/components/ring-chart/index.d.ts +156 -0
- package/lib/typescript/src/components/ring-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/separator/index.d.ts +36 -1
- package/lib/typescript/src/components/separator/index.d.ts.map +1 -1
- package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/typography/index.d.ts +6 -0
- package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-direction.d.ts +20 -0
- package/lib/typescript/src/hooks/use-direction.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +5 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +9 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/text.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +96 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -0
- package/package.json +9 -1
- package/src/components/area-chart/index.tsx +994 -0
- package/src/components/avatar/index.tsx +1 -1
- package/src/components/bar-chart/index.tsx +1048 -0
- package/src/components/bottom-sheet/index.tsx +280 -20
- package/src/components/breadcrumb/index.tsx +365 -0
- package/src/components/chip/index.tsx +2 -2
- package/src/components/direction/index.tsx +20 -21
- package/src/components/frame/index.tsx +17 -0
- package/src/components/heatmap-chart/index.tsx +58 -11
- package/src/components/input-group/index.tsx +2 -2
- package/src/components/line-chart/index.tsx +13 -181
- package/src/components/loader/index.tsx +812 -0
- package/src/components/map/basemap.ts +304 -0
- package/src/components/map/index.tsx +1118 -0
- package/src/components/map/maplibre.ts +201 -0
- package/src/components/message/index.tsx +8 -7
- package/src/components/otp-input/index.tsx +2 -2
- package/src/components/progress/index.tsx +10 -2
- package/src/components/ring-chart/index.tsx +543 -0
- package/src/components/section-rail/index.tsx +2 -2
- package/src/components/separator/index.tsx +77 -19
- package/src/components/shimmer/index.tsx +10 -1
- package/src/components/slider/index.tsx +20 -5
- package/src/components/steps/index.tsx +1 -1
- package/src/components/switch/index.tsx +6 -1
- package/src/components/typography/index.tsx +5 -0
- package/src/hooks/use-direction.ts +45 -0
- package/src/icons/index.tsx +66 -0
- package/src/index.ts +84 -0
- package/src/primitives/text.tsx +25 -7
- package/src/utils/chart.ts +355 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
useEffect,
|
|
8
8
|
useMemo,
|
|
9
9
|
useState,
|
|
10
|
+
type ComponentProps,
|
|
10
11
|
type ReactElement,
|
|
11
12
|
type ReactNode,
|
|
12
13
|
} from 'react';
|
|
@@ -19,10 +20,12 @@ import Animated, {
|
|
|
19
20
|
SlideInDown,
|
|
20
21
|
SlideOutDown,
|
|
21
22
|
runOnJS,
|
|
23
|
+
useAnimatedScrollHandler,
|
|
22
24
|
useAnimatedStyle,
|
|
23
25
|
useSharedValue,
|
|
24
26
|
withSpring,
|
|
25
27
|
withTiming,
|
|
28
|
+
type SharedValue,
|
|
26
29
|
} from 'react-native-reanimated';
|
|
27
30
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
28
31
|
import { tv } from 'tailwind-variants';
|
|
@@ -30,6 +33,7 @@ import { getNativeUI } from '../../native';
|
|
|
30
33
|
import { XIcon } from '../../icons';
|
|
31
34
|
import { Portal } from '../../primitives/portal';
|
|
32
35
|
import { Scrim } from '../../primitives/scrim';
|
|
36
|
+
import { Text } from '../../primitives/text';
|
|
33
37
|
import { cn } from '../../utils/cn';
|
|
34
38
|
|
|
35
39
|
const SPRING = { damping: 22, stiffness: 280, mass: 0.7 } as const;
|
|
@@ -134,6 +138,24 @@ const NativeSheetContext = createContext<{
|
|
|
134
138
|
snapPoints: BottomSheetProps['snapPoints'];
|
|
135
139
|
} | null>(null);
|
|
136
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The wiring between the sheet's drag and a scrolling body inside it. Set by
|
|
143
|
+
* `Content`, filled in by `Body` — the two gestures have to agree on where the
|
|
144
|
+
* list is scrolled to before either can decide whose drag it is.
|
|
145
|
+
*/
|
|
146
|
+
/*
|
|
147
|
+
* Taken off the factory rather than imported by name: the package exports two
|
|
148
|
+
* different `NativeGesture` types, and the one reachable from its entry point
|
|
149
|
+
* is not the one `Gesture.Native()` returns.
|
|
150
|
+
*/
|
|
151
|
+
type ScrollGesture = ReturnType<typeof Gesture.Native>;
|
|
152
|
+
|
|
153
|
+
const SheetSurfaceContext = createContext<{
|
|
154
|
+
scrollGesture: ScrollGesture;
|
|
155
|
+
scrollOffset: SharedValue<number>;
|
|
156
|
+
hasScrollable: SharedValue<boolean>;
|
|
157
|
+
} | null>(null);
|
|
158
|
+
|
|
137
159
|
function BottomSheetRoot({
|
|
138
160
|
children,
|
|
139
161
|
open,
|
|
@@ -195,8 +217,10 @@ export interface BottomSheetContentProps extends ViewProps {
|
|
|
195
217
|
/** Tap on the backdrop closes the sheet. Default true. */
|
|
196
218
|
dismissible?: boolean;
|
|
197
219
|
/**
|
|
198
|
-
* Show a close button in the top
|
|
199
|
-
*
|
|
220
|
+
* Show a close button in the top trailing corner — the right in a
|
|
221
|
+
* left-to-right app, the left in a right-to-left one. On by default for the
|
|
222
|
+
* styled sheet; ignored by the native sheet, which has its own dismiss
|
|
223
|
+
* affordances.
|
|
200
224
|
*/
|
|
201
225
|
showClose?: boolean;
|
|
202
226
|
/**
|
|
@@ -213,15 +237,35 @@ export interface BottomSheetContentProps extends ViewProps {
|
|
|
213
237
|
* optional `expo-blur`; without it this dims, rather than failing.
|
|
214
238
|
*/
|
|
215
239
|
blur?: boolean;
|
|
240
|
+
/**
|
|
241
|
+
* How tall the sheet opens.
|
|
242
|
+
*
|
|
243
|
+
* `auto` sizes to the content, which is right for a sheet that is a handful
|
|
244
|
+
* of rows. `half` and `full` fix the height instead, for content that has to
|
|
245
|
+
* be given the room rather than allowed to ask for it — a list, a form, a
|
|
246
|
+
* document. Either way the sheet is clamped to leave the status bar clear,
|
|
247
|
+
* so `full` is as tall as the screen allows rather than as tall as the screen.
|
|
248
|
+
*
|
|
249
|
+
* On the native sheet this maps onto the platform's detents.
|
|
250
|
+
*/
|
|
251
|
+
size?: 'auto' | 'half' | 'full';
|
|
216
252
|
children?: ReactNode;
|
|
217
253
|
}
|
|
218
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Fractions of the screen the sized sheets aim for. `full` is not 1: a sheet
|
|
257
|
+
* that reached the top would have nothing behind it to read as laid *over*,
|
|
258
|
+
* and the gap is what says the app is still there underneath.
|
|
259
|
+
*/
|
|
260
|
+
const SIZE_FRACTION = { half: 0.5, full: 0.94 } as const;
|
|
261
|
+
|
|
219
262
|
function BottomSheetContent({
|
|
220
263
|
className,
|
|
221
264
|
dismissible = true,
|
|
222
265
|
showClose = true,
|
|
223
266
|
detached = false,
|
|
224
267
|
blur = false,
|
|
268
|
+
size = 'auto',
|
|
225
269
|
children,
|
|
226
270
|
...props
|
|
227
271
|
}: BottomSheetContentProps) {
|
|
@@ -233,6 +277,17 @@ function BottomSheetContent({
|
|
|
233
277
|
const translateY = useSharedValue(0);
|
|
234
278
|
const closeTint = useCSSVariable('--color-muted-foreground');
|
|
235
279
|
|
|
280
|
+
/*
|
|
281
|
+
* The scrolling body's gesture, if there is one. It is built here rather
|
|
282
|
+
* than in `BottomSheet.Body` because the drag below has to name it, and a
|
|
283
|
+
* gesture is composed before its children have mounted. `Body` attaches it
|
|
284
|
+
* to its own scroll view; with no `Body` it is simply never attached, and
|
|
285
|
+
* the drag reverts to owning every touch.
|
|
286
|
+
*/
|
|
287
|
+
const scrollGesture = useMemo(() => Gesture.Native(), []);
|
|
288
|
+
const scrollOffset = useSharedValue(0);
|
|
289
|
+
const hasScrollable = useSharedValue(false);
|
|
290
|
+
|
|
236
291
|
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
237
292
|
|
|
238
293
|
/*
|
|
@@ -256,7 +311,25 @@ function BottomSheetContent({
|
|
|
256
311
|
* is in the middle of, so a tap with any finger travel does nothing.
|
|
257
312
|
*/
|
|
258
313
|
.activeOffsetY([-12, 12])
|
|
314
|
+
/*
|
|
315
|
+
* A scrolling body runs its own gesture, and without this the two
|
|
316
|
+
* compete: whichever wins takes the touch outright, so either the
|
|
317
|
+
* list never scrolls or the sheet never drags. Running them together
|
|
318
|
+
* lets the rule below decide which one a given drag belongs to.
|
|
319
|
+
*/
|
|
320
|
+
.simultaneousWithExternalGesture(scrollGesture)
|
|
259
321
|
.onChange((event) => {
|
|
322
|
+
/*
|
|
323
|
+
* The list gets the drag until it has nothing left to scroll. Once
|
|
324
|
+
* it is at the top the sheet takes over, which is the gesture people
|
|
325
|
+
* already expect: pulling down on a list that cannot go further
|
|
326
|
+
* pulls the sheet instead. The second clause keeps a sheet that is
|
|
327
|
+
* already part-way dragged following the finger, so releasing the
|
|
328
|
+
* drag halfway does not strand it.
|
|
329
|
+
*/
|
|
330
|
+
if (hasScrollable.value && scrollOffset.value > 0 && translateY.value <= 0) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
260
333
|
// Rubber-band when dragging upward, follow the finger downward.
|
|
261
334
|
const next = translateY.value + event.changeY;
|
|
262
335
|
translateY.value = next > 0 ? next : next / 3;
|
|
@@ -276,15 +349,40 @@ function BottomSheetContent({
|
|
|
276
349
|
// Rebuilt only when one of these changes. Built inline it would be a new
|
|
277
350
|
// gesture on every render — and the sheet re-renders while it is being
|
|
278
351
|
// used, each time re-attaching the handler and dropping the live touch.
|
|
279
|
-
[close, screenHeight, translateY]
|
|
352
|
+
[close, screenHeight, translateY, scrollGesture, scrollOffset, hasScrollable]
|
|
280
353
|
);
|
|
281
354
|
|
|
282
355
|
const sheetStyle = useAnimatedStyle(() => ({
|
|
283
356
|
transform: [{ translateY: translateY.value }],
|
|
284
357
|
}));
|
|
285
358
|
|
|
359
|
+
/*
|
|
360
|
+
* A sized sheet is clamped rather than handed its fraction outright.
|
|
361
|
+
* `insets.top` is the status bar and the notch, and content that runs under
|
|
362
|
+
* those is unreadable at exactly the moment the sheet is at its tallest.
|
|
363
|
+
*/
|
|
364
|
+
const sizedHeight =
|
|
365
|
+
size === 'auto'
|
|
366
|
+
? undefined
|
|
367
|
+
: Math.min(
|
|
368
|
+
screenHeight * SIZE_FRACTION[size],
|
|
369
|
+
screenHeight - insets.top - (detached ? 24 : 8)
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const surface = useMemo(
|
|
373
|
+
() => ({ scrollGesture, scrollOffset, hasScrollable }),
|
|
374
|
+
[scrollGesture, scrollOffset, hasScrollable]
|
|
375
|
+
);
|
|
376
|
+
|
|
286
377
|
if (nativeSheet) {
|
|
287
378
|
const { Host, BottomSheet: NativeBottomSheet, RNHostView } = nativeSheet.nativeUI;
|
|
379
|
+
/*
|
|
380
|
+
* `snapPoints` on the root is the finer control and wins; `size` is the
|
|
381
|
+
* shorthand, and maps onto the platform's own detents rather than a
|
|
382
|
+
* height, so a native sheet keeps the system's snapping.
|
|
383
|
+
*/
|
|
384
|
+
const snapPoints =
|
|
385
|
+
nativeSheet.snapPoints ?? (size === 'auto' ? undefined : [size]);
|
|
288
386
|
// The platform owns presentation, so this stays mounted and toggles
|
|
289
387
|
// isPresented rather than unmounting on close.
|
|
290
388
|
//
|
|
@@ -296,7 +394,7 @@ function BottomSheetContent({
|
|
|
296
394
|
<NativeBottomSheet
|
|
297
395
|
isPresented={open}
|
|
298
396
|
onDismiss={dismissible ? close : () => {}}
|
|
299
|
-
snapPoints={
|
|
397
|
+
snapPoints={snapPoints}
|
|
300
398
|
>
|
|
301
399
|
<RNHostView matchContents>
|
|
302
400
|
<BottomSheetContext.Provider value={context}>
|
|
@@ -316,7 +414,7 @@ function BottomSheetContent({
|
|
|
316
414
|
// against, so `100%` measures against nothing and the
|
|
317
415
|
// content lays out wider than the sheet it sits in.
|
|
318
416
|
width: screenWidth,
|
|
319
|
-
minHeight: detentFloor(
|
|
417
|
+
minHeight: detentFloor(snapPoints, screenHeight),
|
|
320
418
|
paddingBottom: Math.max(insets.bottom, 16),
|
|
321
419
|
}}
|
|
322
420
|
>
|
|
@@ -364,24 +462,42 @@ function BottomSheetContent({
|
|
|
364
462
|
// indicator, so it takes plain padding rather than stacking the
|
|
365
463
|
// inset on top of the gap.
|
|
366
464
|
{ paddingBottom: detached ? 16 : Math.max(insets.bottom, 16) },
|
|
465
|
+
sizedHeight === undefined ? null : { height: sizedHeight },
|
|
367
466
|
props.style,
|
|
368
467
|
]}
|
|
369
468
|
>
|
|
370
|
-
<
|
|
371
|
-
<View className="
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
469
|
+
<SheetSurfaceContext.Provider value={surface}>
|
|
470
|
+
<View className="mb-3 self-center">
|
|
471
|
+
<View className="h-1 w-10 rounded-full bg-muted-foreground/30" />
|
|
472
|
+
</View>
|
|
473
|
+
{children}
|
|
474
|
+
{/*
|
|
475
|
+
* Last, and lifted above the content.
|
|
476
|
+
*
|
|
477
|
+
* Drawn before the children it was covered by whichever of them
|
|
478
|
+
* reached the top-right corner — a title spanning the sheet's
|
|
479
|
+
* width is enough — and in React Native a later sibling wins
|
|
480
|
+
* the touch. The button still looked untouched, so the failure
|
|
481
|
+
* read as intermittent: taps landing on the sliver above the
|
|
482
|
+
* title worked, taps anywhere else went to the title and did
|
|
483
|
+
* nothing. `hitSlop` made it worse by growing the part of the
|
|
484
|
+
* target that was already buried.
|
|
485
|
+
*/}
|
|
486
|
+
{showClose ? (
|
|
487
|
+
<Pressable
|
|
488
|
+
accessibilityRole="button"
|
|
489
|
+
accessibilityLabel="Close"
|
|
490
|
+
onPress={close}
|
|
491
|
+
hitSlop={8}
|
|
492
|
+
className="absolute end-4 top-3 z-10 h-8 w-8 items-center justify-center rounded-full bg-muted active:opacity-70"
|
|
493
|
+
>
|
|
494
|
+
<XIcon
|
|
495
|
+
size={16}
|
|
496
|
+
color={typeof closeTint === 'string' ? closeTint : undefined}
|
|
497
|
+
/>
|
|
498
|
+
</Pressable>
|
|
499
|
+
) : null}
|
|
500
|
+
</SheetSurfaceContext.Provider>
|
|
385
501
|
</Animated.View>
|
|
386
502
|
</GestureDetector>
|
|
387
503
|
</View>
|
|
@@ -390,7 +506,151 @@ function BottomSheetContent({
|
|
|
390
506
|
);
|
|
391
507
|
}
|
|
392
508
|
|
|
509
|
+
export interface BottomSheetHeaderProps extends ViewProps {
|
|
510
|
+
className?: string;
|
|
511
|
+
/** Heading for the sheet. Strings are wrapped; anything else is drawn as given. */
|
|
512
|
+
title?: ReactNode;
|
|
513
|
+
/** A line under the title, for what the sheet is asking. */
|
|
514
|
+
description?: ReactNode;
|
|
515
|
+
children?: ReactNode;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* A fixed heading above the sheet's body.
|
|
520
|
+
*
|
|
521
|
+
* It exists mostly to reserve the top-right corner. The close button is
|
|
522
|
+
* absolutely positioned there, so anything full-width at the top of a sheet
|
|
523
|
+
* sits under it — a title is exactly that shape, and the end padding here is
|
|
524
|
+
* what keeps the two from meeting. Being outside `Body` is the other half:
|
|
525
|
+
* it stays put while the content scrolls under it, so the sheet still says
|
|
526
|
+
* what it is once the list has moved.
|
|
527
|
+
*/
|
|
528
|
+
function BottomSheetHeader({
|
|
529
|
+
className,
|
|
530
|
+
title,
|
|
531
|
+
description,
|
|
532
|
+
children,
|
|
533
|
+
...props
|
|
534
|
+
}: BottomSheetHeaderProps) {
|
|
535
|
+
return (
|
|
536
|
+
<View className={cn('gap-1 pb-3 pe-12', className)} {...props}>
|
|
537
|
+
{typeof title === 'string' ? (
|
|
538
|
+
<Text size="lg" weight="semibold">
|
|
539
|
+
{title}
|
|
540
|
+
</Text>
|
|
541
|
+
) : (
|
|
542
|
+
title
|
|
543
|
+
)}
|
|
544
|
+
{typeof description === 'string' ? (
|
|
545
|
+
<Text size="sm" muted>
|
|
546
|
+
{description}
|
|
547
|
+
</Text>
|
|
548
|
+
) : (
|
|
549
|
+
description
|
|
550
|
+
)}
|
|
551
|
+
{children}
|
|
552
|
+
</View>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
BottomSheetHeader.displayName = 'BottomSheet.Header';
|
|
556
|
+
|
|
557
|
+
export interface BottomSheetBodyProps
|
|
558
|
+
extends Omit<ComponentProps<typeof Animated.ScrollView>, 'ref'> {
|
|
559
|
+
className?: string;
|
|
560
|
+
children?: ReactNode;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* The scrolling part of a sized sheet.
|
|
565
|
+
*
|
|
566
|
+
* A plain `ScrollView` works here too, but only one of the two gestures can
|
|
567
|
+
* win a given drag and neither knows about the other, so the list and the
|
|
568
|
+
* sheet fight over every downward swipe. This one reports where it is
|
|
569
|
+
* scrolled to, which is what lets the sheet hold off until the list has run
|
|
570
|
+
* out — pull down on a list at its top and the sheet comes with you, pull
|
|
571
|
+
* down anywhere else and the list scrolls.
|
|
572
|
+
*/
|
|
573
|
+
function BottomSheetBody({
|
|
574
|
+
className,
|
|
575
|
+
children,
|
|
576
|
+
onScroll,
|
|
577
|
+
...props
|
|
578
|
+
}: BottomSheetBodyProps) {
|
|
579
|
+
const surface = useContext(SheetSurfaceContext);
|
|
580
|
+
|
|
581
|
+
useEffect(() => {
|
|
582
|
+
if (!surface) return;
|
|
583
|
+
surface.hasScrollable.value = true;
|
|
584
|
+
return () => {
|
|
585
|
+
surface.hasScrollable.value = false;
|
|
586
|
+
surface.scrollOffset.value = 0;
|
|
587
|
+
};
|
|
588
|
+
}, [surface]);
|
|
589
|
+
|
|
590
|
+
/*
|
|
591
|
+
* Pulled out before the handler rather than read off `surface` inside it.
|
|
592
|
+
* A scroll handler is a worklet, and everything it closes over is copied to
|
|
593
|
+
* the UI thread — closing over the context object would drag the gesture
|
|
594
|
+
* along with it, and a gesture is not a value that can be copied.
|
|
595
|
+
*/
|
|
596
|
+
const scrollOffset = surface?.scrollOffset;
|
|
597
|
+
|
|
598
|
+
const handler = useAnimatedScrollHandler((event) => {
|
|
599
|
+
if (scrollOffset) scrollOffset.value = event.contentOffset.y;
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const body = (
|
|
603
|
+
<Animated.ScrollView
|
|
604
|
+
onScroll={handler}
|
|
605
|
+
scrollEventThrottle={16}
|
|
606
|
+
showsVerticalScrollIndicator={false}
|
|
607
|
+
className={cn('flex-1', className)}
|
|
608
|
+
{...props}
|
|
609
|
+
>
|
|
610
|
+
{children}
|
|
611
|
+
</Animated.ScrollView>
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
/*
|
|
615
|
+
* The detector is what gives the scroll a handler the sheet's drag can name.
|
|
616
|
+
* Without one the two are strangers, and the first to activate takes the
|
|
617
|
+
* touch outright — which is the whole bug this part exists to solve.
|
|
618
|
+
*/
|
|
619
|
+
if (!surface) return body;
|
|
620
|
+
return <GestureDetector gesture={surface.scrollGesture}>{body}</GestureDetector>;
|
|
621
|
+
}
|
|
622
|
+
BottomSheetBody.displayName = 'BottomSheet.Body';
|
|
623
|
+
|
|
624
|
+
export interface BottomSheetFooterProps extends ViewProps {
|
|
625
|
+
className?: string;
|
|
626
|
+
children?: ReactNode;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* A row pinned below the body, for whatever the sheet is asking to be decided.
|
|
631
|
+
* Outside `Body` so it stays reachable however far the content scrolls — an
|
|
632
|
+
* action that has to be scrolled to is one people do not find.
|
|
633
|
+
*/
|
|
634
|
+
function BottomSheetFooter({
|
|
635
|
+
className,
|
|
636
|
+
children,
|
|
637
|
+
...props
|
|
638
|
+
}: BottomSheetFooterProps) {
|
|
639
|
+
return (
|
|
640
|
+
<View
|
|
641
|
+
className={cn('gap-2 border-t border-border pt-3', className)}
|
|
642
|
+
{...props}
|
|
643
|
+
>
|
|
644
|
+
{children}
|
|
645
|
+
</View>
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
BottomSheetFooter.displayName = 'BottomSheet.Footer';
|
|
649
|
+
|
|
393
650
|
export const BottomSheet = Object.assign(BottomSheetRoot, {
|
|
394
651
|
Trigger: BottomSheetTrigger,
|
|
395
652
|
Content: BottomSheetContent,
|
|
653
|
+
Header: BottomSheetHeader,
|
|
654
|
+
Body: BottomSheetBody,
|
|
655
|
+
Footer: BottomSheetFooter,
|
|
396
656
|
});
|