@expokit/cli 0.4.3 → 0.5.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 +30 -3
- package/bin/expo-kit.js +749 -288
- package/bin/lib.mjs +313 -4
- package/package.json +1 -1
- package/registry/_docs/action-sheet.json +1 -1
- package/registry/_docs/alert.json +6 -0
- package/registry/_docs/bottom-sheet.json +6 -0
- package/registry/_docs/bullet.json +6 -0
- package/registry/_docs/dialog.json +1 -1
- package/registry/_docs/drawer.json +3 -3
- package/registry/_docs/flip-calendar.json +106 -0
- package/registry/_docs/glass-search-bar.json +1 -1
- package/registry/_docs/grain.json +6 -1
- package/registry/_docs/isometric-bar.json +6 -0
- package/registry/_docs/liquidglass-tab.json +1 -1
- package/registry/_docs/password-input.json +1 -1
- package/registry/_docs/paywall.json +144 -0
- package/registry/_docs/pixel-bar.json +6 -0
- package/registry/_docs/polar-area.json +6 -0
- package/registry/_docs/qr-code-button.json +9 -3
- package/registry/_docs/ruler-time-picker.json +147 -0
- package/registry/_docs/scratch-card.json +7 -1
- package/registry/_docs/search-bar.json +1 -1
- package/registry/_docs/select.json +2 -2
- package/registry/_docs/slide-to-confirm.json +2 -2
- package/registry/_docs/spinner.json +1 -1
- package/registry/_docs/stateful-button.json +2 -2
- package/registry/_docs/swipe-deck.json +1 -1
- package/registry/_docs/time-picker.json +2 -2
- package/registry/_docs/timeline-chart.json +6 -0
- package/registry/_docs/toast.json +15 -3
- package/registry/_docs/treemap.json +6 -0
- package/registry/_docs/waterfall.json +6 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +6 -4
- package/registry/accordion/files/src/lib/kit.tsx +462 -12
- package/registry/accordion.json +1 -0
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +50 -58
- package/registry/action-sheet/files/src/lib/kit.tsx +462 -12
- package/registry/action-sheet.json +1 -0
- package/registry/alert/files/src/components/ui/alert.tsx +20 -13
- package/registry/alert/files/src/lib/kit.tsx +462 -12
- package/registry/alert.json +3 -4
- package/registry/assistant-app.json +1 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +89 -74
- package/registry/avatar/files/src/lib/kit.tsx +462 -12
- package/registry/avatar.json +1 -0
- package/registry/badge/files/src/components/ui/badge.tsx +36 -36
- package/registry/badge/files/src/lib/kit.tsx +462 -12
- package/registry/badge.json +1 -0
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +39 -44
- package/registry/bottom-nav/files/src/lib/kit.tsx +462 -12
- package/registry/bottom-nav.json +5 -0
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +357 -300
- package/registry/bottom-sheet/files/src/lib/kit.tsx +462 -12
- package/registry/bottom-sheet.json +5 -1
- package/registry/bullet.json +1 -0
- package/registry/button/files/src/components/ui/button.tsx +23 -34
- package/registry/button/files/src/lib/kit.tsx +462 -12
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +6 -9
- package/registry/button-3d/files/src/lib/kit.tsx +462 -12
- package/registry/button-3d.json +1 -0
- package/registry/button.json +1 -0
- package/registry/calendar/files/src/components/ui/calendar.tsx +77 -79
- package/registry/calendar/files/src/lib/kit.tsx +462 -12
- package/registry/calendar.json +1 -0
- package/registry/card/files/src/components/ui/card.tsx +7 -6
- package/registry/card/files/src/lib/kit.tsx +462 -12
- package/registry/card-stack.json +1 -0
- package/registry/card.json +1 -0
- package/registry/carousel.json +1 -0
- package/registry/chat.json +7 -0
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +4 -4
- package/registry/checkbox/files/src/lib/kit.tsx +462 -12
- package/registry/checkbox.json +1 -0
- package/registry/checkout.json +1 -0
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +24 -24
- package/registry/chip-group/files/src/lib/kit.tsx +462 -12
- package/registry/chip-group.json +1 -0
- package/registry/confetti/files/src/components/ui/confetti.tsx +15 -14
- package/registry/confetti.json +3 -6
- package/registry/coupon/files/src/components/ui/coupon.tsx +47 -52
- package/registry/coupon/files/src/components/ui/flip-card.tsx +9 -14
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +80 -101
- package/registry/coupon/files/src/lib/kit.tsx +462 -12
- package/registry/coupon/files/src/lib/ticket-shape.tsx +54 -44
- package/registry/coupon.json +1 -0
- package/registry/cube-pager.json +1 -0
- package/registry/dashboard.json +1 -0
- package/registry/date-picker/files/src/components/ui/calendar.tsx +77 -79
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +64 -71
- package/registry/date-picker/files/src/lib/kit.tsx +462 -12
- package/registry/date-picker.json +1 -0
- package/registry/dialog/files/src/components/ui/dialog.tsx +66 -68
- package/registry/dialog/files/src/lib/kit.tsx +462 -12
- package/registry/dialog.json +1 -0
- package/registry/donut-shop.json +1 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +76 -88
- package/registry/drawer/files/src/lib/kit.tsx +462 -12
- package/registry/drawer.json +1 -0
- package/registry/fab.json +1 -0
- package/registry/fintech-app.json +1 -0
- package/registry/flip-calendar/files/src/components/ui/flip-calendar.tsx +632 -0
- package/registry/flip-calendar/files/src/lib/kit.tsx +672 -0
- package/registry/flip-calendar.json +24 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +9 -14
- package/registry/flip-card/files/src/lib/kit.tsx +462 -12
- package/registry/flip-card.json +1 -0
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +33 -33
- package/registry/floating-input/files/src/lib/kit.tsx +462 -12
- package/registry/floating-input.json +1 -0
- package/registry/food-app.json +1 -0
- package/registry/gift-card.json +1 -0
- package/registry/glass-button.json +5 -0
- package/registry/glass-nav-bar.json +1 -0
- package/registry/glass-search-bar.json +1 -0
- package/registry/glass-segmented.json +1 -0
- package/registry/glass-sheet.json +1 -0
- package/registry/glass-tab-bar.json +5 -0
- package/registry/glass.json +1 -0
- package/registry/grain/files/src/components/ui/grain.tsx +16 -14
- package/registry/grain.json +8 -6
- package/registry/inbox.json +1 -0
- package/registry/isometric-bar.json +1 -0
- package/registry/kit/files/src/lib/kit.tsx +462 -12
- package/registry/kit.json +1 -0
- package/registry/liquidglass-tab/files/src/app/_layout.tsx +0 -1
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +29 -16
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +33 -19
- package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +1 -1
- package/registry/liquidglass-tab.json +1 -0
- package/registry/list-item/files/src/components/ui/list-item.tsx +43 -43
- package/registry/list-item/files/src/lib/kit.tsx +462 -12
- package/registry/list-item.json +1 -0
- package/registry/loyalty-card.json +1 -0
- package/registry/morphing-button-group.json +1 -0
- package/registry/number-ticker.json +1 -0
- package/registry/offers.json +1 -0
- package/registry/onboarding.json +1 -0
- package/registry/orb-reveal.json +1 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +49 -30
- package/registry/otp-input/files/src/lib/kit.tsx +462 -12
- package/registry/otp-input.json +1 -0
- package/registry/parallax-layers.json +1 -0
- package/registry/parallax-scroll.json +1 -0
- package/registry/password-input/files/src/components/ui/floating-input.tsx +33 -33
- package/registry/password-input/files/src/components/ui/password-input.tsx +29 -25
- package/registry/password-input/files/src/lib/kit.tsx +462 -12
- package/registry/password-input.json +1 -0
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +9 -14
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +26 -20
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +80 -101
- package/registry/payment-card/files/src/lib/kit.tsx +462 -12
- package/registry/payment-card.json +1 -0
- package/registry/paywall.json +23 -0
- package/registry/pixel-bar.json +1 -0
- package/registry/polar-area.json +1 -0
- package/registry/profile.json +1 -0
- package/registry/progress/files/src/components/ui/progress.tsx +31 -29
- package/registry/progress/files/src/lib/kit.tsx +462 -12
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +21 -21
- package/registry/progress-steps/files/src/lib/kit.tsx +462 -12
- package/registry/progress-steps.json +1 -0
- package/registry/progress.json +1 -0
- package/registry/qr-code-button.json +1 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +19 -19
- package/registry/radio-group/files/src/lib/kit.tsx +462 -12
- package/registry/radio-group.json +1 -0
- package/registry/rating.json +1 -0
- package/registry/ring-carousel.json +1 -0
- package/registry/ruler-time-picker/files/src/components/ui/ruler-time-picker.tsx +832 -0
- package/registry/ruler-time-picker/files/src/lib/kit.tsx +672 -0
- package/registry/ruler-time-picker.json +26 -0
- package/registry/scratch-card.json +1 -0
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +28 -28
- package/registry/search-bar/files/src/lib/kit.tsx +462 -12
- package/registry/search-bar.json +1 -0
- package/registry/search-results.json +4 -0
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +11 -17
- package/registry/segmented-control/files/src/lib/kit.tsx +462 -12
- package/registry/segmented-control.json +1 -0
- package/registry/select/files/src/components/ui/select.tsx +107 -104
- package/registry/select/files/src/lib/kit.tsx +462 -12
- package/registry/select.json +6 -0
- package/registry/settings.json +1 -0
- package/registry/sign-in.json +1 -0
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +149 -80
- package/registry/skeleton/files/src/lib/kit.tsx +462 -12
- package/registry/skeleton.json +1 -0
- package/registry/slide-to-confirm.json +1 -0
- package/registry/slider/files/src/components/ui/slider.tsx +106 -108
- package/registry/slider/files/src/lib/kit.tsx +462 -12
- package/registry/slider.json +5 -0
- package/registry/spin-wheel.json +1 -0
- package/registry/spinner/files/src/components/ui/spinner.tsx +39 -29
- package/registry/spinner/files/src/lib/kit.tsx +462 -12
- package/registry/spinner.json +6 -0
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +39 -32
- package/registry/stateful-button/files/src/lib/kit.tsx +462 -12
- package/registry/stateful-button.json +5 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +40 -38
- package/registry/stepper/files/src/lib/kit.tsx +462 -12
- package/registry/stepper.json +1 -0
- package/registry/swipe-deck.json +1 -0
- package/registry/swipeable-row.json +4 -0
- package/registry/switch/files/src/components/ui/switch.tsx +41 -47
- package/registry/switch/files/src/lib/kit.tsx +462 -12
- package/registry/switch.json +1 -0
- package/registry/tabs/files/src/components/ui/tabs.tsx +61 -64
- package/registry/tabs/files/src/lib/kit.tsx +462 -12
- package/registry/tabs.json +1 -0
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +55 -42
- package/registry/text-animation/files/src/lib/kit.tsx +462 -12
- package/registry/text-animation.json +7 -0
- package/registry/textarea/files/src/components/ui/textarea.tsx +10 -10
- package/registry/textarea/files/src/lib/kit.tsx +462 -12
- package/registry/textarea.json +1 -0
- package/registry/ticket-card.json +1 -0
- package/registry/ticket-shape/files/src/lib/kit.tsx +462 -12
- package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +54 -44
- package/registry/ticket-shape.json +1 -0
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +80 -101
- package/registry/tilt-card/files/src/lib/kit.tsx +462 -12
- package/registry/tilt-card.json +1 -0
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +136 -131
- package/registry/time-picker/files/src/lib/kit.tsx +462 -12
- package/registry/time-picker.json +1 -0
- package/registry/timeline/files/src/components/ui/avatar.tsx +89 -74
- package/registry/timeline/files/src/components/ui/timeline.tsx +49 -57
- package/registry/timeline/files/src/lib/kit.tsx +462 -12
- package/registry/timeline-chart.json +1 -0
- package/registry/timeline.json +1 -0
- package/registry/toast/files/src/components/ui/toast.tsx +543 -465
- package/registry/toast/files/src/lib/kit.tsx +462 -12
- package/registry/toast.json +7 -5
- package/registry/treemap.json +1 -0
- package/registry/waterfall.json +1 -0
- package/registry/wheel-list.json +1 -0
- package/schema/components.schema.json +1 -0
- package/registry/alert/files/src/components/ui/button.tsx +0 -303
- package/registry/confetti/files/src/lib/kit.tsx +0 -222
- package/registry/grain/files/src/lib/kit.tsx +0 -222
- package/registry/toast/files/src/components/ui/button.tsx +0 -303
|
@@ -2,25 +2,36 @@ import * as Haptics from 'expo-haptics';
|
|
|
2
2
|
import React, { useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
AccessibilityInfo,
|
|
5
|
-
Animated,
|
|
6
|
-
Easing,
|
|
7
5
|
Keyboard,
|
|
8
6
|
Modal,
|
|
9
|
-
PanResponder,
|
|
10
7
|
Platform,
|
|
11
8
|
Pressable,
|
|
12
|
-
ScrollView,
|
|
13
9
|
StyleSheet,
|
|
14
10
|
Text,
|
|
15
11
|
View,
|
|
16
12
|
useWindowDimensions,
|
|
17
13
|
type KeyboardEvent as RNKeyboardEvent,
|
|
18
|
-
type PanResponderGestureState,
|
|
19
14
|
type StyleProp,
|
|
20
15
|
type ViewStyle,
|
|
21
16
|
} from 'react-native';
|
|
17
|
+
import { GestureDetector, GestureHandlerRootView, useNativeGesture, usePanGesture } from 'react-native-gesture-handler';
|
|
18
|
+
import Animated, {
|
|
19
|
+
Easing,
|
|
20
|
+
Extrapolation,
|
|
21
|
+
ReduceMotion,
|
|
22
|
+
cancelAnimation,
|
|
23
|
+
interpolate,
|
|
24
|
+
scrollTo,
|
|
25
|
+
useAnimatedRef,
|
|
26
|
+
useAnimatedScrollHandler,
|
|
27
|
+
useAnimatedStyle,
|
|
28
|
+
useSharedValue,
|
|
29
|
+
withSpring,
|
|
30
|
+
withTiming,
|
|
31
|
+
} from 'react-native-reanimated';
|
|
32
|
+
import { scheduleOnRN } from 'react-native-worklets';
|
|
22
33
|
|
|
23
|
-
import { alpha, motion, useKit } from '@/lib/kit';
|
|
34
|
+
import { alpha, motion, useKit, useLayout } from '@/lib/kit';
|
|
24
35
|
|
|
25
36
|
export type SnapPoint = number | `${number}%`;
|
|
26
37
|
|
|
@@ -32,50 +43,79 @@ export interface BottomSheetProps {
|
|
|
32
43
|
* Heights the sheet rests at: points or percentages of the screen (sorted for you). Omit to size
|
|
33
44
|
* the sheet to its content (capped below the status bar; taller content scrolls).
|
|
34
45
|
*/
|
|
35
|
-
snapPoints?: SnapPoint[];
|
|
46
|
+
snapPoints?: SnapPoint[] | undefined;
|
|
47
|
+
|
|
36
48
|
/** Which snap point it opens at (default 0, the lowest). */
|
|
37
|
-
initialSnap?: number;
|
|
49
|
+
initialSnap?: number | undefined;
|
|
50
|
+
|
|
38
51
|
/** Controlled snap index: change it while open to move the sheet. */
|
|
39
|
-
snapIndex?: number;
|
|
40
|
-
|
|
52
|
+
snapIndex?: number | undefined;
|
|
53
|
+
|
|
54
|
+
onSnapChange?: ((index: number) => void) | undefined;
|
|
55
|
+
|
|
41
56
|
/** Allow closing by dragging down / tapping the backdrop (default true). */
|
|
42
|
-
dismissible?: boolean;
|
|
57
|
+
dismissible?: boolean | undefined;
|
|
58
|
+
|
|
43
59
|
/** Heading under the grabber. */
|
|
44
|
-
title?: string;
|
|
60
|
+
title?: string | undefined;
|
|
61
|
+
|
|
45
62
|
/** Muted line under the title. */
|
|
46
|
-
subtitle?: string;
|
|
63
|
+
subtitle?: string | undefined;
|
|
64
|
+
|
|
47
65
|
/** Node at the trailing end of the title row (e.g. a Reset or Done link). */
|
|
48
|
-
headerRight?: React.ReactNode;
|
|
66
|
+
headerRight?: React.ReactNode | undefined;
|
|
67
|
+
|
|
49
68
|
/** Round ✕ button in the header (default: shown with a title when dismissible and there's no headerRight). */
|
|
50
|
-
showClose?: boolean;
|
|
69
|
+
showClose?: boolean | undefined;
|
|
70
|
+
|
|
71
|
+
/** Screen-reader name of the close button (default: the kit's `labels.close`). */
|
|
72
|
+
closeLabel?: string | undefined;
|
|
73
|
+
|
|
51
74
|
/** Show the grabber pill (default true). */
|
|
52
|
-
handle?: boolean;
|
|
75
|
+
handle?: boolean | undefined;
|
|
76
|
+
|
|
53
77
|
/**
|
|
54
78
|
* Wrap the body in a ScrollView. Below the top snap point a vertical drag on the body resizes
|
|
55
79
|
* the sheet; at the top snap point the body scrolls, and pulling down from its top collapses it.
|
|
56
80
|
*/
|
|
57
|
-
scrollable?: boolean;
|
|
81
|
+
scrollable?: boolean | undefined;
|
|
82
|
+
|
|
58
83
|
/** Let vertical drags on the body move the sheet (default: true when `scrollable`). Keep false if children contain their own ScrollView. */
|
|
59
|
-
dragFromContent?: boolean;
|
|
84
|
+
dragFromContent?: boolean | undefined;
|
|
85
|
+
|
|
60
86
|
/** Pinned to the bottom of the visible sheet at every snap point (e.g. an Apply button). */
|
|
61
|
-
footer?: React.ReactNode;
|
|
87
|
+
footer?: React.ReactNode | undefined;
|
|
88
|
+
|
|
62
89
|
/** Lift the sheet above the on-screen keyboard (default true). */
|
|
63
|
-
avoidKeyboard?: boolean;
|
|
90
|
+
avoidKeyboard?: boolean | undefined;
|
|
91
|
+
|
|
64
92
|
/** Light impact on open and a selection tick on each snap (default true). */
|
|
65
|
-
haptics?: boolean;
|
|
93
|
+
haptics?: boolean | undefined;
|
|
94
|
+
|
|
66
95
|
/** Max backdrop dim at the highest snap point, 0–1 (default 1 = theme overlay). */
|
|
67
|
-
backdropOpacity?: number;
|
|
96
|
+
backdropOpacity?: number | undefined;
|
|
97
|
+
|
|
68
98
|
/** Extra bottom padding for the home indicator (default: platform safe inset). */
|
|
69
|
-
bottomInset?: number;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
99
|
+
bottomInset?: number | undefined;
|
|
100
|
+
|
|
101
|
+
accessibilityLabel?: string | undefined;
|
|
102
|
+
|
|
103
|
+
children?: React.ReactNode | undefined;
|
|
104
|
+
|
|
105
|
+
style?: StyleProp<ViewStyle> | undefined;
|
|
106
|
+
|
|
107
|
+
contentStyle?: StyleProp<ViewStyle> | undefined;
|
|
108
|
+
|
|
74
109
|
}
|
|
75
110
|
|
|
111
|
+
|
|
76
112
|
const DEFAULT_BOTTOM_INSET = Platform.select({ ios: 20, android: 12, default: 16 }) ?? 16;
|
|
77
113
|
/** Kit gentle, firmed up — glides between detents without bouncing past them. */
|
|
78
114
|
const SNAP_SPRING = { ...motion.gentle, damping: 29, stiffness: 220, mass: 0.9 };
|
|
115
|
+
/** The sheet handles reduced motion itself, so Reanimated must not also skip its animations. */
|
|
116
|
+
const KEEP = ReduceMotion.Never;
|
|
117
|
+
const SNAPPY = { ...motion.snappy, reduceMotion: KEEP };
|
|
118
|
+
const BOUNCY = { ...motion.bouncy, reduceMotion: KEEP };
|
|
79
119
|
/** Extra sheet below the screen edge, so pulling up past the top snap never shows a gap. */
|
|
80
120
|
const OVERDRAG = 120;
|
|
81
121
|
/** Never let the sheet's top go closer than this to the top of the screen. */
|
|
@@ -87,6 +127,7 @@ function toPx(p: SnapPoint, screen: number) {
|
|
|
87
127
|
|
|
88
128
|
/** Diminishing resistance past the limits (approaches `dimension`). */
|
|
89
129
|
function rubber(distance: number, dimension: number) {
|
|
130
|
+
'worklet';
|
|
90
131
|
return (distance * dimension * 0.55) / (dimension + 0.55 * distance);
|
|
91
132
|
}
|
|
92
133
|
|
|
@@ -95,7 +136,8 @@ function rubber(distance: number, dimension: number) {
|
|
|
95
136
|
* settles on the snap point projected from your release velocity (a fling skips ahead or
|
|
96
137
|
* dismisses) with a haptic tick, and the scrim dims in step with its height. Content can scroll
|
|
97
138
|
* once the sheet is fully expanded, a footer stays pinned at every detent, and the keyboard lifts
|
|
98
|
-
* it.
|
|
139
|
+
* it. Drags and snaps run on the UI thread (Reanimated + Gesture Handler), so a busy JS thread
|
|
140
|
+
* never stalls them.
|
|
99
141
|
*/
|
|
100
142
|
export function BottomSheet({
|
|
101
143
|
open,
|
|
@@ -109,6 +151,7 @@ export function BottomSheet({
|
|
|
109
151
|
subtitle,
|
|
110
152
|
headerRight,
|
|
111
153
|
showClose,
|
|
154
|
+
closeLabel,
|
|
112
155
|
handle = true,
|
|
113
156
|
scrollable = false,
|
|
114
157
|
dragFromContent,
|
|
@@ -122,10 +165,12 @@ export function BottomSheet({
|
|
|
122
165
|
style,
|
|
123
166
|
contentStyle,
|
|
124
167
|
}: BottomSheetProps) {
|
|
125
|
-
const { colors, radius, dark, font } = useKit();
|
|
168
|
+
const { colors, radius, dark, font, labels } = useKit();
|
|
126
169
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
127
170
|
const reduceMotion = useReduceMotion();
|
|
128
171
|
const { height: screen } = useWindowDimensions();
|
|
172
|
+
// Full width on portrait phones; a centred, width-capped card on foldables, tablets and landscape.
|
|
173
|
+
const { sheetWidth } = useLayout();
|
|
129
174
|
|
|
130
175
|
// No snap points: one detent at the content's own height, measured before the sheet slides in.
|
|
131
176
|
const auto = !snapPoints || snapPoints.length === 0;
|
|
@@ -148,89 +193,97 @@ export function BottomSheet({
|
|
|
148
193
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
149
194
|
[snapKey, screen],
|
|
150
195
|
);
|
|
151
|
-
const max = heights[heights.length - 1];
|
|
196
|
+
const max = heights[heights.length - 1] ?? 0;
|
|
152
197
|
// translateY for each snap (0 = top snap) and for closed.
|
|
153
198
|
const stops = heights.map((h) => max - h);
|
|
154
199
|
const closed = max + 24;
|
|
155
200
|
const last = stops.length - 1;
|
|
156
201
|
|
|
157
|
-
//
|
|
158
|
-
const y =
|
|
159
|
-
const lift =
|
|
160
|
-
const grip =
|
|
202
|
+
// UI-thread values: sheet position, keyboard lift, grabber stretch, the body's scroll offset.
|
|
203
|
+
const y = useSharedValue(closed);
|
|
204
|
+
const lift = useSharedValue(0);
|
|
205
|
+
const grip = useSharedValue(0);
|
|
206
|
+
const index = useSharedValue(initialSnap);
|
|
207
|
+
const busy = useSharedValue(false); // a snap animation is running
|
|
208
|
+
const scrollY = useSharedValue(0);
|
|
209
|
+
// Per-drag: where the sheet was when the finger took it, which of sheet / list it moves, and the list offset to hold.
|
|
210
|
+
const origin = useSharedValue(0);
|
|
211
|
+
const scrolling = useSharedValue(false);
|
|
212
|
+
const scrollHold = useSharedValue(0);
|
|
213
|
+
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
|
161
214
|
|
|
162
215
|
const [mounted, setMounted] = useState(open);
|
|
163
216
|
const [expanded, setExpanded] = useState(false); // at the top snap → body scrolls
|
|
164
217
|
const [kbPad, setKbPad] = useState(0);
|
|
165
218
|
const shown = useRef(false); // auto: slid in once measured
|
|
166
|
-
const index = useRef(initialSnap);
|
|
167
|
-
const scrollY = useRef(0);
|
|
168
219
|
const rootRef = useRef<ComponentRef<typeof View>>(null);
|
|
169
220
|
|
|
170
221
|
const bodyDrags = dragFromContent ?? scrollable;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const settle = (to: number, velocity = 0,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (finished) after?.();
|
|
192
|
-
});
|
|
222
|
+
|
|
223
|
+
const announce = (k: number) => {
|
|
224
|
+
onSnapChange?.(k);
|
|
225
|
+
if (haptics) Haptics.selectionAsync().catch(() => {});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const settle = (to: number, velocity = 0, close = false) => {
|
|
229
|
+
'worklet';
|
|
230
|
+
busy.set(true);
|
|
231
|
+
const done = (finished?: boolean) => {
|
|
232
|
+
'worklet';
|
|
233
|
+
if (!finished) return;
|
|
234
|
+
busy.set(false);
|
|
235
|
+
if (close) scheduleOnRN(onClose);
|
|
236
|
+
};
|
|
237
|
+
y.set(
|
|
238
|
+
reduceMotion
|
|
239
|
+
? withTiming(to, { duration: 200, easing: Easing.out(Easing.cubic), reduceMotion: KEEP }, done)
|
|
240
|
+
: withSpring(to, { ...SNAP_SPRING, velocity, overshootClamping: to === closed, reduceMotion: KEEP }, done),
|
|
241
|
+
);
|
|
193
242
|
};
|
|
194
243
|
|
|
195
244
|
const snapTo = (i: number, velocity = 0) => {
|
|
196
|
-
|
|
197
|
-
const k = Math.max(0, Math.min(
|
|
198
|
-
if (index.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
index.current = k;
|
|
203
|
-
setExpanded(k === s.length - 1);
|
|
204
|
-
settle(s[k], velocity);
|
|
245
|
+
'worklet';
|
|
246
|
+
const k = Math.max(0, Math.min(last, i));
|
|
247
|
+
if (index.get() !== k) scheduleOnRN(announce, k);
|
|
248
|
+
index.set(k);
|
|
249
|
+
scheduleOnRN(setExpanded, k === last);
|
|
250
|
+
settle(stops[k] ?? closed, velocity);
|
|
205
251
|
};
|
|
206
252
|
|
|
207
|
-
const dismiss = (velocity = 0) =>
|
|
253
|
+
const dismiss = (velocity = 0) => {
|
|
254
|
+
'worklet';
|
|
255
|
+
settle(closed, velocity, true);
|
|
256
|
+
};
|
|
208
257
|
|
|
209
258
|
const enter = () => {
|
|
210
259
|
shown.current = true;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
scrollY.
|
|
215
|
-
index.
|
|
216
|
-
setExpanded
|
|
260
|
+
const k = Math.max(0, Math.min(last, snapIndex ?? initialSnap));
|
|
261
|
+
y.set(closed);
|
|
262
|
+
lift.set(0);
|
|
263
|
+
scrollY.set(0);
|
|
264
|
+
index.set(k);
|
|
265
|
+
scheduleOnRN(setExpanded, k === last);
|
|
217
266
|
if (haptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
|
218
|
-
settle(stops[
|
|
267
|
+
settle(stops[k] ?? closed);
|
|
219
268
|
};
|
|
220
269
|
|
|
270
|
+
// Mount as soon as `open` turns on (render-time update, so no extra effect pass).
|
|
271
|
+
if (open && !mounted) setMounted(true);
|
|
272
|
+
|
|
221
273
|
useEffect(() => {
|
|
222
274
|
if (open) {
|
|
223
|
-
setMounted(true);
|
|
224
275
|
shown.current = false;
|
|
225
276
|
// Off-screen until there is a height to open to.
|
|
226
|
-
y.
|
|
277
|
+
y.set(screen);
|
|
227
278
|
if (measured) enter();
|
|
228
279
|
} else if (mounted) {
|
|
229
280
|
// Exit is quicker than the entrance: a short ease-in, no spring.
|
|
230
281
|
Keyboard.dismiss();
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
282
|
+
lift.set(withTiming(0, { duration: 160, reduceMotion: KEEP }));
|
|
283
|
+
y.set(
|
|
284
|
+
withTiming(closed, { duration: 200, easing: Easing.in(Easing.quad), reduceMotion: KEEP }, (finished) => {
|
|
285
|
+
if (finished) scheduleOnRN(setMounted, false);
|
|
286
|
+
}),
|
|
234
287
|
);
|
|
235
288
|
}
|
|
236
289
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -240,16 +293,13 @@ export function BottomSheet({
|
|
|
240
293
|
useEffect(() => {
|
|
241
294
|
if (!open || !auto || !measured) return;
|
|
242
295
|
if (!shown.current) enter();
|
|
243
|
-
else if (!
|
|
244
|
-
pos.current = 0;
|
|
245
|
-
y.setValue(0);
|
|
246
|
-
}
|
|
296
|
+
else if (!busy.get() && index.get() === 0) y.set(0);
|
|
247
297
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
248
298
|
}, [open, auto, measured, autoH]);
|
|
249
299
|
|
|
250
300
|
// Controlled snap index.
|
|
251
301
|
useEffect(() => {
|
|
252
|
-
if (open && mounted && snapIndex !== undefined && snapIndex !== index.
|
|
302
|
+
if (open && mounted && snapIndex !== undefined && snapIndex !== index.get()) snapTo(snapIndex);
|
|
253
303
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
254
304
|
}, [snapIndex]);
|
|
255
305
|
|
|
@@ -259,11 +309,11 @@ export function BottomSheet({
|
|
|
259
309
|
if (!mounted || !avoidKeyboard) return;
|
|
260
310
|
const ios = Platform.OS === 'ios';
|
|
261
311
|
const apply = (overlap: number, duration: number) => {
|
|
262
|
-
const visible =
|
|
312
|
+
const visible = max - y.get();
|
|
263
313
|
const room = Math.max(0, screen - TOP_GAP - visible);
|
|
264
314
|
const up = Math.min(overlap, room);
|
|
265
315
|
setKbPad(overlap - up);
|
|
266
|
-
|
|
316
|
+
lift.set(withTiming(-up, { duration, easing: Easing.out(Easing.cubic), reduceMotion: KEEP }));
|
|
267
317
|
};
|
|
268
318
|
const show = Keyboard.addListener(ios ? 'keyboardWillShow' : 'keyboardDidShow', (e: RNKeyboardEvent) => {
|
|
269
319
|
const duration = e.duration || 220;
|
|
@@ -273,97 +323,122 @@ export function BottomSheet({
|
|
|
273
323
|
});
|
|
274
324
|
const hide = Keyboard.addListener(ios ? 'keyboardWillHide' : 'keyboardDidHide', (e: RNKeyboardEvent) => {
|
|
275
325
|
setKbPad(0);
|
|
276
|
-
|
|
326
|
+
lift.set(withTiming(0, { duration: e?.duration || 200, easing: Easing.out(Easing.cubic), reduceMotion: KEEP }));
|
|
277
327
|
});
|
|
278
328
|
return () => {
|
|
279
329
|
show.remove();
|
|
280
330
|
hide.remove();
|
|
281
331
|
};
|
|
282
|
-
}, [mounted, avoidKeyboard, screen, lift]);
|
|
332
|
+
}, [mounted, avoidKeyboard, screen, max, y, lift]);
|
|
283
333
|
|
|
284
334
|
// Escape closes on web (the Modal handles Android back).
|
|
285
335
|
useEffect(() => {
|
|
286
336
|
if (!mounted || Platform.OS !== 'web' || typeof document === 'undefined') return;
|
|
287
337
|
const onKey = (e: KeyboardEvent) => {
|
|
288
|
-
if (e.key === 'Escape' &&
|
|
338
|
+
if (e.key === 'Escape' && dismissible) dismiss();
|
|
289
339
|
};
|
|
290
340
|
document.addEventListener('keydown', onKey);
|
|
291
341
|
return () => document.removeEventListener('keydown', onKey);
|
|
292
342
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
293
|
-
}, [mounted]);
|
|
294
|
-
|
|
295
|
-
// One drag model for the grabber/header (claims any touch) and the body (captures vertical
|
|
296
|
-
// drags only, so taps, horizontal scrollers and — at the top snap — scrolling still work).
|
|
297
|
-
const pans = useRef(
|
|
298
|
-
(() => {
|
|
299
|
-
const origin = { start: 0 };
|
|
300
|
-
const base = {
|
|
301
|
-
onPanResponderTerminationRequest: () => false,
|
|
302
|
-
onPanResponderGrant: () => {
|
|
303
|
-
origin.start = pos.current;
|
|
304
|
-
Animated.spring(grip, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
305
|
-
if (!moving.current) return;
|
|
306
|
-
moving.current = false;
|
|
307
|
-
y.stopAnimation((v) => {
|
|
308
|
-
if (typeof v === 'number') origin.start = v;
|
|
309
|
-
});
|
|
310
|
-
},
|
|
311
|
-
onPanResponderMove: (_: unknown, g: PanResponderGestureState) => {
|
|
312
|
-
const { closed: bottom, stops: s, dismissible: canClose } = live.current;
|
|
313
|
-
let next = origin.start + g.dy;
|
|
314
|
-
if (next < 0) next = -rubber(-next, OVERDRAG);
|
|
315
|
-
if (!canClose && next > s[0]) next = s[0] + rubber(next - s[0], OVERDRAG);
|
|
316
|
-
pos.current = Math.min(next, bottom + 40);
|
|
317
|
-
y.setValue(pos.current);
|
|
318
|
-
},
|
|
319
|
-
onPanResponderRelease: (_: unknown, g: PanResponderGestureState) => {
|
|
320
|
-
Animated.spring(grip, { toValue: 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
321
|
-
const { stops: s, closed: bottom, dismissible: canClose } = live.current;
|
|
322
|
-
const current = origin.start + g.dy;
|
|
323
|
-
const projected = current + g.vy * 220;
|
|
324
|
-
const targets = canClose ? [...s, bottom] : s;
|
|
325
|
-
let best = 0;
|
|
326
|
-
targets.forEach((t, i) => {
|
|
327
|
-
if (Math.abs(t - projected) < Math.abs(targets[best] - projected)) best = i;
|
|
328
|
-
});
|
|
329
|
-
if (canClose && (best === s.length || (g.vy > 1.6 && current > s[0]))) dismiss(g.vy);
|
|
330
|
-
else snapTo(Math.min(best, s.length - 1), g.vy);
|
|
331
|
-
},
|
|
332
|
-
onPanResponderTerminate: () => {
|
|
333
|
-
Animated.spring(grip, { toValue: 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
334
|
-
snapTo(index.current);
|
|
335
|
-
},
|
|
336
|
-
};
|
|
337
|
-
const grab = PanResponder.create({
|
|
338
|
-
...base,
|
|
339
|
-
onStartShouldSetPanResponder: () => true,
|
|
340
|
-
onMoveShouldSetPanResponder: () => true,
|
|
341
|
-
});
|
|
342
|
-
const body = PanResponder.create({
|
|
343
|
-
...base,
|
|
344
|
-
onMoveShouldSetPanResponderCapture: (_, g) => {
|
|
345
|
-
const l = live.current;
|
|
346
|
-
if (!l.bodyDrags) return false;
|
|
347
|
-
if (Math.abs(g.dy) < 8 || Math.abs(g.dy) < Math.abs(g.dx) * 1.3) return false;
|
|
348
|
-
if (!l.scrollable || !l.expanded) return true;
|
|
349
|
-
// Fully expanded: the list scrolls; pulling down from its very top collapses the sheet.
|
|
350
|
-
return g.dy > 0 && scrollY.current <= 0;
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
return { grab, body };
|
|
354
|
-
})(),
|
|
355
|
-
).current;
|
|
343
|
+
}, [mounted, dismissible]);
|
|
356
344
|
|
|
357
|
-
|
|
345
|
+
// One drag model, run on the UI thread, for the grabber/header and the body.
|
|
346
|
+
// `dy` is how far the finger already travelled before the drag activated: the sheet picks up from here, no jump.
|
|
347
|
+
const take = (dy: number) => {
|
|
348
|
+
'worklet';
|
|
349
|
+
cancelAnimation(y);
|
|
350
|
+
busy.set(false);
|
|
351
|
+
origin.set(y.get() - dy);
|
|
352
|
+
};
|
|
353
|
+
const follow = (dy: number) => {
|
|
354
|
+
'worklet';
|
|
355
|
+
let next = origin.get() + dy;
|
|
356
|
+
if (next < 0) next = -rubber(-next, OVERDRAG);
|
|
357
|
+
const top = stops[0] ?? 0;
|
|
358
|
+
if (!dismissible && next > top) next = top + rubber(next - top, OVERDRAG);
|
|
359
|
+
y.set(Math.min(next, closed + 40));
|
|
360
|
+
};
|
|
361
|
+
const release = (dy: number, vy: number) => {
|
|
362
|
+
'worklet';
|
|
363
|
+
const current = origin.get() + dy;
|
|
364
|
+
// Project where the fling would carry the sheet (vy is points per second).
|
|
365
|
+
const projected = current + vy * 0.22;
|
|
366
|
+
const targets = dismissible ? [...stops, closed] : stops;
|
|
367
|
+
let best = 0;
|
|
368
|
+
targets.forEach((t, i) => {
|
|
369
|
+
if (Math.abs(t - projected) < Math.abs((targets[best] ?? t) - projected)) best = i;
|
|
370
|
+
});
|
|
371
|
+
if (dismissible && (best === stops.length || (vy > 1600 && current > (stops[0] ?? 0)))) dismiss(vy);
|
|
372
|
+
else snapTo(Math.min(best, last), vy);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Grabber and header: any vertical drag moves the sheet; taps still reach the close button.
|
|
376
|
+
const grab = usePanGesture({
|
|
377
|
+
activeOffsetY: [-4, 4],
|
|
378
|
+
onBegin: () => {
|
|
379
|
+
grip.set(withSpring(1, SNAPPY));
|
|
380
|
+
},
|
|
381
|
+
onActivate: (e) => take(e.translationY),
|
|
382
|
+
onUpdate: (e) => follow(e.translationY),
|
|
383
|
+
onDeactivate: (e) => {
|
|
384
|
+
if (e.canceled) snapTo(index.get());
|
|
385
|
+
else release(e.translationY, e.velocityY);
|
|
386
|
+
},
|
|
387
|
+
onFinalize: () => {
|
|
388
|
+
grip.set(withSpring(0, BOUNCY));
|
|
389
|
+
},
|
|
390
|
+
});
|
|
358
391
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
392
|
+
// Body: vertical drags only, so taps and horizontal scrollers still work. At the top snap of a
|
|
393
|
+
// scrollable sheet the list scrolls instead, and a pull down from its very top moves the sheet.
|
|
394
|
+
const scrolls = scrollable || auto; // auto height scrolls when its content is taller than the screen allows
|
|
395
|
+
const handoff = scrollable && expanded;
|
|
396
|
+
const scrollGesture = useNativeGesture({ enabled: scrolls });
|
|
397
|
+
const body = usePanGesture({
|
|
398
|
+
enabled: bodyDrags,
|
|
399
|
+
activeOffsetY: [-8, 8],
|
|
400
|
+
failOffsetX: [-8, 8],
|
|
401
|
+
simultaneousWith: scrollGesture,
|
|
402
|
+
onActivate: (e) => {
|
|
403
|
+
scrollHold.set(scrollY.get());
|
|
404
|
+
scrolling.set(handoff && (e.translationY < 0 || scrollY.get() > 0));
|
|
405
|
+
if (!scrolling.get()) take(e.translationY);
|
|
406
|
+
},
|
|
407
|
+
onUpdate: (e) => {
|
|
408
|
+
if (scrolling.get()) {
|
|
409
|
+
// Scrolled back to the top and still pulling down: hand the drag to the sheet.
|
|
410
|
+
if (scrollY.get() > 0 || e.changeY <= 0) return;
|
|
411
|
+
scrolling.set(false);
|
|
412
|
+
scrollHold.set(0);
|
|
413
|
+
origin.set(y.get() - e.translationY);
|
|
414
|
+
}
|
|
415
|
+
if (scrolls) scrollTo(scrollRef, 0, scrollHold.get(), false);
|
|
416
|
+
follow(e.translationY);
|
|
417
|
+
},
|
|
418
|
+
onDeactivate: (e) => {
|
|
419
|
+
if (scrolling.get()) return;
|
|
420
|
+
if (e.canceled) snapTo(index.get());
|
|
421
|
+
else release(e.translationY, e.velocityY);
|
|
422
|
+
},
|
|
364
423
|
});
|
|
424
|
+
|
|
425
|
+
const onScroll = useAnimatedScrollHandler((e) => {
|
|
426
|
+
scrollY.set(e.contentOffset.y);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const lowest = Math.max(1, stops[0] ?? 0);
|
|
430
|
+
const restDim = backdropOpacity * (heights.length > 1 ? 0.7 : 1);
|
|
431
|
+
const dimStyle = useAnimatedStyle(() => ({
|
|
432
|
+
opacity: interpolate(y.get(), [0, lowest, closed], [backdropOpacity, restDim, 0], Extrapolation.CLAMP),
|
|
433
|
+
}));
|
|
434
|
+
const sheetStyle = useAnimatedStyle(() => ({ transform: [{ translateY: y.get() + lift.get() }] }));
|
|
435
|
+
const gripStyle = useAnimatedStyle(() => ({ transform: [{ scaleX: 1 + 0.3 * grip.get() }] }));
|
|
365
436
|
// Keeps the footer on the screen's bottom edge at every detent; it follows the sheet down when closing.
|
|
366
|
-
const
|
|
437
|
+
const footerStyle = useAnimatedStyle(() => ({
|
|
438
|
+
transform: [{ translateY: interpolate(y.get(), [0, lowest], [0, -lowest], Extrapolation.CLAMP) }],
|
|
439
|
+
}));
|
|
440
|
+
|
|
441
|
+
if (!mounted) return null;
|
|
367
442
|
|
|
368
443
|
const surface = dark ? colors.popover : colors.card;
|
|
369
444
|
const hairline = alpha(colors.foreground, dark ? 0.1 : 0.06);
|
|
@@ -371,23 +446,24 @@ export function BottomSheet({
|
|
|
371
446
|
const closeButton = showClose ?? (!!title && dismissible && !headerRight);
|
|
372
447
|
const hasHeader = !!(title || subtitle || headerRight || closeButton);
|
|
373
448
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
449
|
+
const content = scrolls ? (
|
|
450
|
+
<GestureDetector gesture={scrollGesture}>
|
|
451
|
+
<Animated.ScrollView
|
|
452
|
+
ref={scrollRef}
|
|
453
|
+
style={styles.flex}
|
|
454
|
+
onContentSizeChange={auto ? (_w, h) => setContentH(Math.ceil(h)) : undefined}
|
|
455
|
+
contentContainerStyle={[styles.content, { paddingBottom: 12 + (footer ? footerH : 0) + kbPad }, contentStyle]}
|
|
456
|
+
scrollEnabled={expanded}
|
|
457
|
+
bounces={false}
|
|
458
|
+
overScrollMode="never"
|
|
459
|
+
showsVerticalScrollIndicator={expanded && (!auto || headerH + contentH + bottomInset > screen - TOP_GAP)}
|
|
460
|
+
keyboardShouldPersistTaps="handled"
|
|
461
|
+
scrollEventThrottle={16}
|
|
462
|
+
onScroll={onScroll}
|
|
463
|
+
>
|
|
464
|
+
{children}
|
|
465
|
+
</Animated.ScrollView>
|
|
466
|
+
</GestureDetector>
|
|
391
467
|
) : (
|
|
392
468
|
<View
|
|
393
469
|
onLayout={auto ? (e) => setContentH(Math.ceil(e.nativeEvent.layout.height)) : undefined}
|
|
@@ -405,129 +481,120 @@ export function BottomSheet({
|
|
|
405
481
|
statusBarTranslucent
|
|
406
482
|
onRequestClose={() => (dismissible ? dismiss() : snapTo(0))}
|
|
407
483
|
>
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
{
|
|
425
|
-
height: max + OVERDRAG,
|
|
426
|
-
opacity: measured ? 1 : 0,
|
|
427
|
-
shadowOpacity: dark ? 0.5 : 0.12,
|
|
428
|
-
transform: [{ translateY: Animated.add(y, lift) }],
|
|
429
|
-
},
|
|
430
|
-
style,
|
|
431
|
-
]}
|
|
432
|
-
>
|
|
433
|
-
<View
|
|
434
|
-
style={[
|
|
435
|
-
styles.surface,
|
|
436
|
-
{
|
|
437
|
-
paddingBottom: OVERDRAG + (footer ? 0 : bottomInset),
|
|
438
|
-
backgroundColor: surface,
|
|
439
|
-
borderTopLeftRadius: corner,
|
|
440
|
-
borderTopRightRadius: corner,
|
|
441
|
-
borderColor: hairline,
|
|
442
|
-
shadowOpacity: dark ? 0.35 : 0.06,
|
|
443
|
-
},
|
|
444
|
-
]}
|
|
484
|
+
{/* A Modal is its own native root, so gestures inside it need their own handler root. */}
|
|
485
|
+
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
|
|
486
|
+
<View ref={rootRef} style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
487
|
+
<Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: colors.overlay }, dimStyle]}>
|
|
488
|
+
<Pressable
|
|
489
|
+
style={StyleSheet.absoluteFill}
|
|
490
|
+
accessibilityRole="button"
|
|
491
|
+
accessibilityLabel={labels.closeSheet}
|
|
492
|
+
onPress={() => dismissible && dismiss()}
|
|
493
|
+
/>
|
|
494
|
+
</Animated.View>
|
|
495
|
+
<View style={styles.dock} pointerEvents="box-none">
|
|
496
|
+
<Animated.View
|
|
497
|
+
accessibilityViewIsModal
|
|
498
|
+
accessibilityLabel={accessibilityLabel ?? title}
|
|
499
|
+
onAccessibilityEscape={() => dismissible && dismiss()}
|
|
500
|
+
style={[styles.sheet, { height: max + OVERDRAG, width: sheetWidth, opacity: measured ? 1 : 0 }, sheetStyle, style]}
|
|
445
501
|
>
|
|
446
|
-
<View
|
|
447
|
-
{
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
{
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
{title ? (
|
|
477
|
-
<Text accessibilityRole="header" style={[styles.title, ff, { color: colors.foreground }]} numberOfLines={1}>
|
|
478
|
-
{title}
|
|
479
|
-
</Text>
|
|
480
|
-
) : null}
|
|
481
|
-
{subtitle ? (
|
|
482
|
-
<Text style={[styles.subtitle, ff, { color: colors.mutedForeground }]} numberOfLines={1}>
|
|
483
|
-
{subtitle}
|
|
484
|
-
</Text>
|
|
485
|
-
) : null}
|
|
486
|
-
</View>
|
|
487
|
-
{headerRight}
|
|
488
|
-
{closeButton ? (
|
|
489
|
-
<Pressable
|
|
490
|
-
onPress={() => (dismissible ? dismiss() : onClose())}
|
|
491
|
-
hitSlop={10}
|
|
492
|
-
accessibilityRole="button"
|
|
493
|
-
accessibilityLabel="Close"
|
|
494
|
-
style={({ pressed }) => [styles.close, { backgroundColor: alpha(colors.foreground, pressed ? 0.14 : 0.08) }]}
|
|
502
|
+
<View
|
|
503
|
+
style={[
|
|
504
|
+
styles.surface,
|
|
505
|
+
{
|
|
506
|
+
paddingBottom: OVERDRAG + (footer ? 0 : bottomInset),
|
|
507
|
+
backgroundColor: surface,
|
|
508
|
+
borderTopLeftRadius: corner,
|
|
509
|
+
borderTopRightRadius: corner,
|
|
510
|
+
borderColor: hairline,
|
|
511
|
+
// Ambient + contact shadow as boxShadow: identical on iOS, Android and web.
|
|
512
|
+
boxShadow: `0px -8px 64px rgba(0,0,0,${dark ? 0.5 : 0.12}), 0px -1px 6px rgba(0,0,0,${dark ? 0.35 : 0.06})`,
|
|
513
|
+
},
|
|
514
|
+
]}
|
|
515
|
+
>
|
|
516
|
+
<GestureDetector gesture={grab}>
|
|
517
|
+
<View style={styles.grab} onLayout={auto ? (e) => setHeaderH(Math.ceil(e.nativeEvent.layout.height)) : undefined}>
|
|
518
|
+
{handle ? (
|
|
519
|
+
<View
|
|
520
|
+
style={styles.handleArea}
|
|
521
|
+
accessible
|
|
522
|
+
accessibilityRole="adjustable"
|
|
523
|
+
accessibilityLabel={labels.sheetGrabber}
|
|
524
|
+
accessibilityHint={labels.sheetResizeHint}
|
|
525
|
+
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
|
|
526
|
+
onAccessibilityAction={(e) => {
|
|
527
|
+
const at = index.get();
|
|
528
|
+
if (e.nativeEvent.actionName === 'increment') snapTo(at + 1);
|
|
529
|
+
else if (at === 0 && dismissible) dismiss();
|
|
530
|
+
else snapTo(at - 1);
|
|
531
|
+
}}
|
|
495
532
|
>
|
|
496
|
-
<View
|
|
497
|
-
|
|
498
|
-
|
|
533
|
+
<Animated.View
|
|
534
|
+
style={[
|
|
535
|
+
styles.handle,
|
|
536
|
+
{ borderRadius: radius.capsule(5), backgroundColor: alpha(colors.foreground, dark ? 0.26 : 0.18) },
|
|
537
|
+
gripStyle,
|
|
538
|
+
]}
|
|
539
|
+
/>
|
|
540
|
+
</View>
|
|
541
|
+
) : null}
|
|
542
|
+
{hasHeader ? (
|
|
543
|
+
<View style={[styles.header, !handle && { paddingTop: 20 }]}>
|
|
544
|
+
<View style={styles.headerText}>
|
|
545
|
+
{title ? (
|
|
546
|
+
<Text accessibilityRole="header" style={[styles.title, ff, { color: colors.foreground }]} numberOfLines={1}>
|
|
547
|
+
{title}
|
|
548
|
+
</Text>
|
|
549
|
+
) : null}
|
|
550
|
+
{subtitle ? (
|
|
551
|
+
<Text style={[styles.subtitle, ff, { color: colors.mutedForeground }]} numberOfLines={1}>
|
|
552
|
+
{subtitle}
|
|
553
|
+
</Text>
|
|
554
|
+
) : null}
|
|
555
|
+
</View>
|
|
556
|
+
{headerRight}
|
|
557
|
+
{closeButton ? (
|
|
558
|
+
<Pressable
|
|
559
|
+
onPress={() => (dismissible ? dismiss() : onClose())}
|
|
560
|
+
hitSlop={10}
|
|
561
|
+
accessibilityRole="button"
|
|
562
|
+
accessibilityLabel={closeLabel ?? labels.close}
|
|
563
|
+
style={({ pressed }) => [styles.close, { backgroundColor: alpha(colors.foreground, pressed ? 0.14 : 0.08) }]}
|
|
564
|
+
>
|
|
565
|
+
<View style={[styles.closeBar, { backgroundColor: colors.mutedForeground, transform: [{ rotate: '45deg' }] }]} />
|
|
566
|
+
<View style={[styles.closeBar, { backgroundColor: colors.mutedForeground, transform: [{ rotate: '-45deg' }] }]} />
|
|
567
|
+
</Pressable>
|
|
568
|
+
) : null}
|
|
569
|
+
</View>
|
|
499
570
|
) : null}
|
|
500
571
|
</View>
|
|
572
|
+
</GestureDetector>
|
|
573
|
+
<GestureDetector gesture={body}>
|
|
574
|
+
<View style={styles.flex}>{content}</View>
|
|
575
|
+
</GestureDetector>
|
|
576
|
+
{footer ? (
|
|
577
|
+
<Animated.View
|
|
578
|
+
onLayout={(e) => setFooterH(e.nativeEvent.layout.height)}
|
|
579
|
+
style={[
|
|
580
|
+
styles.footer,
|
|
581
|
+
{ bottom: OVERDRAG, paddingBottom: bottomInset, backgroundColor: surface, borderTopColor: hairline },
|
|
582
|
+
footerStyle,
|
|
583
|
+
]}
|
|
584
|
+
>
|
|
585
|
+
{footer}
|
|
586
|
+
</Animated.View>
|
|
501
587
|
) : null}
|
|
502
588
|
</View>
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
</View>
|
|
506
|
-
{footer ? (
|
|
507
|
-
<Animated.View
|
|
508
|
-
onLayout={(e) => setFooterH(e.nativeEvent.layout.height)}
|
|
509
|
-
style={[
|
|
510
|
-
styles.footer,
|
|
511
|
-
{
|
|
512
|
-
bottom: OVERDRAG,
|
|
513
|
-
paddingBottom: bottomInset,
|
|
514
|
-
backgroundColor: surface,
|
|
515
|
-
borderTopColor: hairline,
|
|
516
|
-
transform: [{ translateY: footerY }],
|
|
517
|
-
},
|
|
518
|
-
]}
|
|
519
|
-
>
|
|
520
|
-
{footer}
|
|
521
|
-
</Animated.View>
|
|
522
|
-
) : null}
|
|
523
|
-
</View>
|
|
524
|
-
</Animated.View>
|
|
589
|
+
</Animated.View>
|
|
590
|
+
</View>
|
|
525
591
|
</View>
|
|
526
|
-
</
|
|
592
|
+
</GestureHandlerRootView>
|
|
527
593
|
</Modal>
|
|
528
594
|
);
|
|
529
595
|
}
|
|
530
596
|
|
|
597
|
+
|
|
531
598
|
function useReduceMotion() {
|
|
532
599
|
const [reduce, setReduce] = useState(false);
|
|
533
600
|
useEffect(() => {
|
|
@@ -549,21 +616,11 @@ const styles = StyleSheet.create({
|
|
|
549
616
|
// No text selection while dragging with a mouse on web.
|
|
550
617
|
grab: Platform.OS === 'web' ? ({ userSelect: 'none', cursor: 'grab' } as object) : {},
|
|
551
618
|
dock: { position: 'absolute', left: 0, right: 0, bottom: -OVERDRAG, alignItems: 'center' },
|
|
552
|
-
sheet: {
|
|
553
|
-
width: '100%',
|
|
554
|
-
maxWidth: 640,
|
|
555
|
-
shadowColor: '#000',
|
|
556
|
-
shadowRadius: 32,
|
|
557
|
-
shadowOffset: { width: 0, height: -8 },
|
|
558
|
-
elevation: 24,
|
|
559
|
-
},
|
|
619
|
+
sheet: { maxWidth: '100%' },
|
|
560
620
|
surface: {
|
|
561
621
|
flex: 1,
|
|
562
622
|
borderWidth: StyleSheet.hairlineWidth,
|
|
563
623
|
borderBottomWidth: 0,
|
|
564
|
-
shadowColor: '#000',
|
|
565
|
-
shadowRadius: 3,
|
|
566
|
-
shadowOffset: { width: 0, height: -1 },
|
|
567
624
|
},
|
|
568
625
|
handleArea: { alignItems: 'center', paddingTop: 8, paddingBottom: 10, minHeight: 28 },
|
|
569
626
|
handle: { width: 36, height: 5 },
|