@expokit/cli 0.4.4 → 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 +310 -3
- 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/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/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/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 +363 -11
- package/registry/accordion.json +1 -0
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +43 -48
- package/registry/action-sheet/files/src/lib/kit.tsx +363 -11
- package/registry/action-sheet.json +1 -0
- package/registry/alert/files/src/components/ui/alert.tsx +19 -12
- package/registry/alert/files/src/lib/kit.tsx +363 -11
- package/registry/alert.json +3 -4
- package/registry/assistant-app.json +1 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +84 -66
- package/registry/avatar/files/src/lib/kit.tsx +363 -11
- package/registry/avatar.json +1 -0
- package/registry/badge/files/src/components/ui/badge.tsx +34 -34
- package/registry/badge/files/src/lib/kit.tsx +363 -11
- package/registry/badge.json +1 -0
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +28 -27
- package/registry/bottom-nav/files/src/lib/kit.tsx +363 -11
- package/registry/bottom-nav.json +5 -0
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +353 -289
- package/registry/bottom-sheet/files/src/lib/kit.tsx +363 -11
- package/registry/bottom-sheet.json +5 -1
- package/registry/bullet.json +1 -0
- package/registry/button/files/src/components/ui/button.tsx +18 -16
- package/registry/button/files/src/lib/kit.tsx +363 -11
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +2 -2
- package/registry/button-3d/files/src/lib/kit.tsx +363 -11
- package/registry/button-3d.json +1 -0
- package/registry/button.json +1 -0
- package/registry/calendar/files/src/components/ui/calendar.tsx +74 -76
- package/registry/calendar/files/src/lib/kit.tsx +363 -11
- package/registry/calendar.json +1 -0
- package/registry/card/files/src/components/ui/card.tsx +3 -3
- package/registry/card/files/src/lib/kit.tsx +363 -11
- 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 +363 -11
- package/registry/checkbox.json +1 -0
- package/registry/checkout.json +1 -0
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +22 -22
- package/registry/chip-group/files/src/lib/kit.tsx +363 -11
- 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 +42 -42
- package/registry/coupon/files/src/components/ui/flip-card.tsx +6 -5
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +76 -91
- package/registry/coupon/files/src/lib/kit.tsx +363 -11
- 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 +74 -76
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +55 -56
- package/registry/date-picker/files/src/lib/kit.tsx +363 -11
- package/registry/date-picker.json +1 -0
- package/registry/dialog/files/src/components/ui/dialog.tsx +60 -56
- package/registry/dialog/files/src/lib/kit.tsx +363 -11
- package/registry/dialog.json +1 -0
- package/registry/donut-shop.json +1 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +73 -69
- package/registry/drawer/files/src/lib/kit.tsx +363 -11
- 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 +92 -86
- package/registry/flip-calendar/files/src/lib/kit.tsx +363 -11
- package/registry/flip-calendar.json +7 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +6 -5
- package/registry/flip-card/files/src/lib/kit.tsx +363 -11
- 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 +363 -11
- 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 +363 -11
- 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 +23 -9
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +2 -2
- 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 +42 -42
- package/registry/list-item/files/src/lib/kit.tsx +363 -11
- 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 +48 -29
- package/registry/otp-input/files/src/lib/kit.tsx +363 -11
- 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 +363 -11
- package/registry/password-input.json +1 -0
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +6 -5
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +17 -17
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +76 -91
- package/registry/payment-card/files/src/lib/kit.tsx +363 -11
- package/registry/payment-card.json +1 -0
- package/registry/paywall.json +1 -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 +29 -29
- package/registry/progress/files/src/lib/kit.tsx +363 -11
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +20 -20
- package/registry/progress-steps/files/src/lib/kit.tsx +363 -11
- 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 +363 -11
- 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 +98 -82
- package/registry/ruler-time-picker/files/src/lib/kit.tsx +363 -11
- package/registry/ruler-time-picker.json +7 -0
- package/registry/scratch-card.json +1 -0
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +25 -21
- package/registry/search-bar/files/src/lib/kit.tsx +363 -11
- 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 +8 -7
- package/registry/segmented-control/files/src/lib/kit.tsx +363 -11
- package/registry/segmented-control.json +1 -0
- package/registry/select/files/src/components/ui/select.tsx +93 -88
- package/registry/select/files/src/lib/kit.tsx +363 -11
- 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 +363 -11
- package/registry/skeleton.json +1 -0
- package/registry/slide-to-confirm.json +1 -0
- package/registry/slider/files/src/components/ui/slider.tsx +103 -96
- package/registry/slider/files/src/lib/kit.tsx +363 -11
- 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 +363 -11
- 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 +363 -11
- package/registry/stateful-button.json +5 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +38 -36
- package/registry/stepper/files/src/lib/kit.tsx +363 -11
- 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 +39 -42
- package/registry/switch/files/src/lib/kit.tsx +363 -11
- package/registry/switch.json +1 -0
- package/registry/tabs/files/src/components/ui/tabs.tsx +59 -62
- package/registry/tabs/files/src/lib/kit.tsx +363 -11
- 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 +363 -11
- 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 +363 -11
- package/registry/textarea.json +1 -0
- package/registry/ticket-card.json +1 -0
- package/registry/ticket-shape/files/src/lib/kit.tsx +363 -11
- 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 +76 -91
- package/registry/tilt-card/files/src/lib/kit.tsx +363 -11
- package/registry/tilt-card.json +1 -0
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +122 -119
- package/registry/time-picker/files/src/lib/kit.tsx +363 -11
- package/registry/time-picker.json +1 -0
- package/registry/timeline/files/src/components/ui/avatar.tsx +84 -66
- package/registry/timeline/files/src/components/ui/timeline.tsx +44 -41
- package/registry/timeline/files/src/lib/kit.tsx +363 -11
- package/registry/timeline-chart.json +1 -0
- package/registry/timeline.json +1 -0
- package/registry/toast/files/src/components/ui/toast.tsx +541 -463
- package/registry/toast/files/src/lib/kit.tsx +363 -11
- 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 -290
- package/registry/confetti/files/src/lib/kit.tsx +0 -320
- package/registry/grain/files/src/lib/kit.tsx +0 -320
- package/registry/toast/files/src/components/ui/button.tsx +0 -290
|
@@ -2,23 +2,34 @@ 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
34
|
import { alpha, motion, useKit, useLayout } from '@/lib/kit';
|
|
24
35
|
|
|
@@ -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,7 +165,7 @@ 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();
|
|
@@ -150,89 +193,97 @@ export function BottomSheet({
|
|
|
150
193
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
151
194
|
[snapKey, screen],
|
|
152
195
|
);
|
|
153
|
-
const max = heights[heights.length - 1];
|
|
196
|
+
const max = heights[heights.length - 1] ?? 0;
|
|
154
197
|
// translateY for each snap (0 = top snap) and for closed.
|
|
155
198
|
const stops = heights.map((h) => max - h);
|
|
156
199
|
const closed = max + 24;
|
|
157
200
|
const last = stops.length - 1;
|
|
158
201
|
|
|
159
|
-
//
|
|
160
|
-
const y =
|
|
161
|
-
const lift =
|
|
162
|
-
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>();
|
|
163
214
|
|
|
164
215
|
const [mounted, setMounted] = useState(open);
|
|
165
216
|
const [expanded, setExpanded] = useState(false); // at the top snap → body scrolls
|
|
166
217
|
const [kbPad, setKbPad] = useState(0);
|
|
167
218
|
const shown = useRef(false); // auto: slid in once measured
|
|
168
|
-
const index = useRef(initialSnap);
|
|
169
|
-
const scrollY = useRef(0);
|
|
170
219
|
const rootRef = useRef<ComponentRef<typeof View>>(null);
|
|
171
220
|
|
|
172
221
|
const bodyDrags = dragFromContent ?? scrollable;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const settle = (to: number, velocity = 0,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (finished) after?.();
|
|
194
|
-
});
|
|
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
|
+
);
|
|
195
242
|
};
|
|
196
243
|
|
|
197
244
|
const snapTo = (i: number, velocity = 0) => {
|
|
198
|
-
|
|
199
|
-
const k = Math.max(0, Math.min(
|
|
200
|
-
if (index.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
index.current = k;
|
|
205
|
-
setExpanded(k === s.length - 1);
|
|
206
|
-
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);
|
|
207
251
|
};
|
|
208
252
|
|
|
209
|
-
const dismiss = (velocity = 0) =>
|
|
253
|
+
const dismiss = (velocity = 0) => {
|
|
254
|
+
'worklet';
|
|
255
|
+
settle(closed, velocity, true);
|
|
256
|
+
};
|
|
210
257
|
|
|
211
258
|
const enter = () => {
|
|
212
259
|
shown.current = true;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
scrollY.
|
|
217
|
-
index.
|
|
218
|
-
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);
|
|
219
266
|
if (haptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
|
220
|
-
settle(stops[
|
|
267
|
+
settle(stops[k] ?? closed);
|
|
221
268
|
};
|
|
222
269
|
|
|
270
|
+
// Mount as soon as `open` turns on (render-time update, so no extra effect pass).
|
|
271
|
+
if (open && !mounted) setMounted(true);
|
|
272
|
+
|
|
223
273
|
useEffect(() => {
|
|
224
274
|
if (open) {
|
|
225
|
-
setMounted(true);
|
|
226
275
|
shown.current = false;
|
|
227
276
|
// Off-screen until there is a height to open to.
|
|
228
|
-
y.
|
|
277
|
+
y.set(screen);
|
|
229
278
|
if (measured) enter();
|
|
230
279
|
} else if (mounted) {
|
|
231
280
|
// Exit is quicker than the entrance: a short ease-in, no spring.
|
|
232
281
|
Keyboard.dismiss();
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
}),
|
|
236
287
|
);
|
|
237
288
|
}
|
|
238
289
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -242,16 +293,13 @@ export function BottomSheet({
|
|
|
242
293
|
useEffect(() => {
|
|
243
294
|
if (!open || !auto || !measured) return;
|
|
244
295
|
if (!shown.current) enter();
|
|
245
|
-
else if (!
|
|
246
|
-
pos.current = 0;
|
|
247
|
-
y.setValue(0);
|
|
248
|
-
}
|
|
296
|
+
else if (!busy.get() && index.get() === 0) y.set(0);
|
|
249
297
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
250
298
|
}, [open, auto, measured, autoH]);
|
|
251
299
|
|
|
252
300
|
// Controlled snap index.
|
|
253
301
|
useEffect(() => {
|
|
254
|
-
if (open && mounted && snapIndex !== undefined && snapIndex !== index.
|
|
302
|
+
if (open && mounted && snapIndex !== undefined && snapIndex !== index.get()) snapTo(snapIndex);
|
|
255
303
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
256
304
|
}, [snapIndex]);
|
|
257
305
|
|
|
@@ -261,11 +309,11 @@ export function BottomSheet({
|
|
|
261
309
|
if (!mounted || !avoidKeyboard) return;
|
|
262
310
|
const ios = Platform.OS === 'ios';
|
|
263
311
|
const apply = (overlap: number, duration: number) => {
|
|
264
|
-
const visible =
|
|
312
|
+
const visible = max - y.get();
|
|
265
313
|
const room = Math.max(0, screen - TOP_GAP - visible);
|
|
266
314
|
const up = Math.min(overlap, room);
|
|
267
315
|
setKbPad(overlap - up);
|
|
268
|
-
|
|
316
|
+
lift.set(withTiming(-up, { duration, easing: Easing.out(Easing.cubic), reduceMotion: KEEP }));
|
|
269
317
|
};
|
|
270
318
|
const show = Keyboard.addListener(ios ? 'keyboardWillShow' : 'keyboardDidShow', (e: RNKeyboardEvent) => {
|
|
271
319
|
const duration = e.duration || 220;
|
|
@@ -275,97 +323,122 @@ export function BottomSheet({
|
|
|
275
323
|
});
|
|
276
324
|
const hide = Keyboard.addListener(ios ? 'keyboardWillHide' : 'keyboardDidHide', (e: RNKeyboardEvent) => {
|
|
277
325
|
setKbPad(0);
|
|
278
|
-
|
|
326
|
+
lift.set(withTiming(0, { duration: e?.duration || 200, easing: Easing.out(Easing.cubic), reduceMotion: KEEP }));
|
|
279
327
|
});
|
|
280
328
|
return () => {
|
|
281
329
|
show.remove();
|
|
282
330
|
hide.remove();
|
|
283
331
|
};
|
|
284
|
-
}, [mounted, avoidKeyboard, screen, lift]);
|
|
332
|
+
}, [mounted, avoidKeyboard, screen, max, y, lift]);
|
|
285
333
|
|
|
286
334
|
// Escape closes on web (the Modal handles Android back).
|
|
287
335
|
useEffect(() => {
|
|
288
336
|
if (!mounted || Platform.OS !== 'web' || typeof document === 'undefined') return;
|
|
289
337
|
const onKey = (e: KeyboardEvent) => {
|
|
290
|
-
if (e.key === 'Escape' &&
|
|
338
|
+
if (e.key === 'Escape' && dismissible) dismiss();
|
|
291
339
|
};
|
|
292
340
|
document.addEventListener('keydown', onKey);
|
|
293
341
|
return () => document.removeEventListener('keydown', onKey);
|
|
294
342
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
295
|
-
}, [mounted]);
|
|
296
|
-
|
|
297
|
-
// One drag model for the grabber/header (claims any touch) and the body (captures vertical
|
|
298
|
-
// drags only, so taps, horizontal scrollers and — at the top snap — scrolling still work).
|
|
299
|
-
const pans = useRef(
|
|
300
|
-
(() => {
|
|
301
|
-
const origin = { start: 0 };
|
|
302
|
-
const base = {
|
|
303
|
-
onPanResponderTerminationRequest: () => false,
|
|
304
|
-
onPanResponderGrant: () => {
|
|
305
|
-
origin.start = pos.current;
|
|
306
|
-
Animated.spring(grip, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
307
|
-
if (!moving.current) return;
|
|
308
|
-
moving.current = false;
|
|
309
|
-
y.stopAnimation((v) => {
|
|
310
|
-
if (typeof v === 'number') origin.start = v;
|
|
311
|
-
});
|
|
312
|
-
},
|
|
313
|
-
onPanResponderMove: (_: unknown, g: PanResponderGestureState) => {
|
|
314
|
-
const { closed: bottom, stops: s, dismissible: canClose } = live.current;
|
|
315
|
-
let next = origin.start + g.dy;
|
|
316
|
-
if (next < 0) next = -rubber(-next, OVERDRAG);
|
|
317
|
-
if (!canClose && next > s[0]) next = s[0] + rubber(next - s[0], OVERDRAG);
|
|
318
|
-
pos.current = Math.min(next, bottom + 40);
|
|
319
|
-
y.setValue(pos.current);
|
|
320
|
-
},
|
|
321
|
-
onPanResponderRelease: (_: unknown, g: PanResponderGestureState) => {
|
|
322
|
-
Animated.spring(grip, { toValue: 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
323
|
-
const { stops: s, closed: bottom, dismissible: canClose } = live.current;
|
|
324
|
-
const current = origin.start + g.dy;
|
|
325
|
-
const projected = current + g.vy * 220;
|
|
326
|
-
const targets = canClose ? [...s, bottom] : s;
|
|
327
|
-
let best = 0;
|
|
328
|
-
targets.forEach((t, i) => {
|
|
329
|
-
if (Math.abs(t - projected) < Math.abs(targets[best] - projected)) best = i;
|
|
330
|
-
});
|
|
331
|
-
if (canClose && (best === s.length || (g.vy > 1.6 && current > s[0]))) dismiss(g.vy);
|
|
332
|
-
else snapTo(Math.min(best, s.length - 1), g.vy);
|
|
333
|
-
},
|
|
334
|
-
onPanResponderTerminate: () => {
|
|
335
|
-
Animated.spring(grip, { toValue: 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
336
|
-
snapTo(index.current);
|
|
337
|
-
},
|
|
338
|
-
};
|
|
339
|
-
const grab = PanResponder.create({
|
|
340
|
-
...base,
|
|
341
|
-
onStartShouldSetPanResponder: () => true,
|
|
342
|
-
onMoveShouldSetPanResponder: () => true,
|
|
343
|
-
});
|
|
344
|
-
const body = PanResponder.create({
|
|
345
|
-
...base,
|
|
346
|
-
onMoveShouldSetPanResponderCapture: (_, g) => {
|
|
347
|
-
const l = live.current;
|
|
348
|
-
if (!l.bodyDrags) return false;
|
|
349
|
-
if (Math.abs(g.dy) < 8 || Math.abs(g.dy) < Math.abs(g.dx) * 1.3) return false;
|
|
350
|
-
if (!l.scrollable || !l.expanded) return true;
|
|
351
|
-
// Fully expanded: the list scrolls; pulling down from its very top collapses the sheet.
|
|
352
|
-
return g.dy > 0 && scrollY.current <= 0;
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
return { grab, body };
|
|
356
|
-
})(),
|
|
357
|
-
).current;
|
|
343
|
+
}, [mounted, dismissible]);
|
|
358
344
|
|
|
359
|
-
|
|
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
|
+
});
|
|
391
|
+
|
|
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
|
+
},
|
|
423
|
+
});
|
|
360
424
|
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
inputRange: [0, lowest, closed],
|
|
364
|
-
outputRange: [backdropOpacity, backdropOpacity * (heights.length > 1 ? 0.7 : 1), 0],
|
|
365
|
-
extrapolate: 'clamp',
|
|
425
|
+
const onScroll = useAnimatedScrollHandler((e) => {
|
|
426
|
+
scrollY.set(e.contentOffset.y);
|
|
366
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() }] }));
|
|
367
436
|
// Keeps the footer on the screen's bottom edge at every detent; it follows the sheet down when closing.
|
|
368
|
-
const
|
|
437
|
+
const footerStyle = useAnimatedStyle(() => ({
|
|
438
|
+
transform: [{ translateY: interpolate(y.get(), [0, lowest], [0, -lowest], Extrapolation.CLAMP) }],
|
|
439
|
+
}));
|
|
440
|
+
|
|
441
|
+
if (!mounted) return null;
|
|
369
442
|
|
|
370
443
|
const surface = dark ? colors.popover : colors.card;
|
|
371
444
|
const hairline = alpha(colors.foreground, dark ? 0.1 : 0.06);
|
|
@@ -373,23 +446,24 @@ export function BottomSheet({
|
|
|
373
446
|
const closeButton = showClose ?? (!!title && dismissible && !headerRight);
|
|
374
447
|
const hasHeader = !!(title || subtitle || headerRight || closeButton);
|
|
375
448
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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>
|
|
393
467
|
) : (
|
|
394
468
|
<View
|
|
395
469
|
onLayout={auto ? (e) => setContentH(Math.ceil(e.nativeEvent.layout.height)) : undefined}
|
|
@@ -407,130 +481,120 @@ export function BottomSheet({
|
|
|
407
481
|
statusBarTranslucent
|
|
408
482
|
onRequestClose={() => (dismissible ? dismiss() : snapTo(0))}
|
|
409
483
|
>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
{
|
|
427
|
-
height: max + OVERDRAG,
|
|
428
|
-
width: sheetWidth,
|
|
429
|
-
opacity: measured ? 1 : 0,
|
|
430
|
-
transform: [{ translateY: Animated.add(y, lift) }],
|
|
431
|
-
},
|
|
432
|
-
style,
|
|
433
|
-
]}
|
|
434
|
-
>
|
|
435
|
-
<View
|
|
436
|
-
style={[
|
|
437
|
-
styles.surface,
|
|
438
|
-
{
|
|
439
|
-
paddingBottom: OVERDRAG + (footer ? 0 : bottomInset),
|
|
440
|
-
backgroundColor: surface,
|
|
441
|
-
borderTopLeftRadius: corner,
|
|
442
|
-
borderTopRightRadius: corner,
|
|
443
|
-
borderColor: hairline,
|
|
444
|
-
// Ambient + contact shadow as boxShadow: identical on iOS, Android and web.
|
|
445
|
-
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})`,
|
|
446
|
-
},
|
|
447
|
-
]}
|
|
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]}
|
|
448
501
|
>
|
|
449
|
-
<View
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
{title ? (
|
|
480
|
-
<Text accessibilityRole="header" style={[styles.title, ff, { color: colors.foreground }]} numberOfLines={1}>
|
|
481
|
-
{title}
|
|
482
|
-
</Text>
|
|
483
|
-
) : null}
|
|
484
|
-
{subtitle ? (
|
|
485
|
-
<Text style={[styles.subtitle, ff, { color: colors.mutedForeground }]} numberOfLines={1}>
|
|
486
|
-
{subtitle}
|
|
487
|
-
</Text>
|
|
488
|
-
) : null}
|
|
489
|
-
</View>
|
|
490
|
-
{headerRight}
|
|
491
|
-
{closeButton ? (
|
|
492
|
-
<Pressable
|
|
493
|
-
onPress={() => (dismissible ? dismiss() : onClose())}
|
|
494
|
-
hitSlop={10}
|
|
495
|
-
accessibilityRole="button"
|
|
496
|
-
accessibilityLabel="Close"
|
|
497
|
-
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
|
+
}}
|
|
498
532
|
>
|
|
499
|
-
<View
|
|
500
|
-
|
|
501
|
-
|
|
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>
|
|
502
570
|
) : null}
|
|
503
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>
|
|
504
587
|
) : null}
|
|
505
588
|
</View>
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
</View>
|
|
509
|
-
{footer ? (
|
|
510
|
-
<Animated.View
|
|
511
|
-
onLayout={(e) => setFooterH(e.nativeEvent.layout.height)}
|
|
512
|
-
style={[
|
|
513
|
-
styles.footer,
|
|
514
|
-
{
|
|
515
|
-
bottom: OVERDRAG,
|
|
516
|
-
paddingBottom: bottomInset,
|
|
517
|
-
backgroundColor: surface,
|
|
518
|
-
borderTopColor: hairline,
|
|
519
|
-
transform: [{ translateY: footerY }],
|
|
520
|
-
},
|
|
521
|
-
]}
|
|
522
|
-
>
|
|
523
|
-
{footer}
|
|
524
|
-
</Animated.View>
|
|
525
|
-
) : null}
|
|
526
|
-
</View>
|
|
527
|
-
</Animated.View>
|
|
589
|
+
</Animated.View>
|
|
590
|
+
</View>
|
|
528
591
|
</View>
|
|
529
|
-
</
|
|
592
|
+
</GestureHandlerRootView>
|
|
530
593
|
</Modal>
|
|
531
594
|
);
|
|
532
595
|
}
|
|
533
596
|
|
|
597
|
+
|
|
534
598
|
function useReduceMotion() {
|
|
535
599
|
const [reduce, setReduce] = useState(false);
|
|
536
600
|
useEffect(() => {
|