@oxyhq/bloom 0.30.4 → 0.30.5
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/lib/commonjs/bottom-sheet/BottomSheetBase.js +731 -0
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -0
- package/lib/commonjs/bottom-sheet/index.js +24 -694
- package/lib/commonjs/bottom-sheet/index.js.map +1 -1
- package/lib/commonjs/bottom-sheet/index.web.js +64 -0
- package/lib/commonjs/bottom-sheet/index.web.js.map +1 -0
- package/lib/commonjs/dialog/Dialog.js +2 -2
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +2 -2
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/dialog/SheetShell.js +2 -2
- package/lib/commonjs/dialog/SheetShell.js.map +1 -1
- package/lib/commonjs/index.js +44 -44
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +66 -66
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +728 -0
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -0
- package/lib/module/bottom-sheet/index.js +27 -696
- package/lib/module/bottom-sheet/index.js.map +1 -1
- package/lib/module/bottom-sheet/index.web.js +63 -0
- package/lib/module/bottom-sheet/index.web.js.map +1 -0
- package/lib/module/dialog/Dialog.js +1 -1
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +1 -1
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/dialog/SheetShell.js +1 -1
- package/lib/module/dialog/SheetShell.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +127 -0
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -0
- package/lib/typescript/commonjs/bottom-sheet/index.d.ts +2 -90
- package/lib/typescript/commonjs/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/index.web.d.ts +7 -0
- package/lib/typescript/commonjs/bottom-sheet/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +127 -0
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -0
- package/lib/typescript/module/bottom-sheet/index.d.ts +2 -90
- package/lib/typescript/module/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/index.web.d.ts +7 -0
- package/lib/typescript/module/bottom-sheet/index.web.d.ts.map +1 -0
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__tests__/BottomSheetWebFork.test.ts +66 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +853 -0
- package/src/bottom-sheet/index.tsx +26 -821
- package/src/bottom-sheet/index.web.tsx +66 -0
- package/src/index.web.ts +2 -2
|
@@ -1,32 +1,18 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import { forwardRef
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { Modal, Platform, StyleSheet } from 'react-native';
|
|
4
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
5
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
3
6
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Platform,
|
|
10
|
-
type ViewStyle,
|
|
11
|
-
type StyleProp,
|
|
12
|
-
} from 'react-native';
|
|
13
|
-
import { Gesture, GestureDetector, GestureHandlerRootView, type GestureType } from 'react-native-gesture-handler';
|
|
14
|
-
import { Z_INDEX } from '../styles/z-index';
|
|
15
|
-
import Animated, {
|
|
16
|
-
interpolate,
|
|
17
|
-
runOnJS,
|
|
18
|
-
type SharedValue,
|
|
19
|
-
useAnimatedScrollHandler,
|
|
20
|
-
useAnimatedStyle,
|
|
21
|
-
useSharedValue,
|
|
22
|
-
withSpring,
|
|
23
|
-
withTiming,
|
|
24
|
-
} from 'react-native-reanimated';
|
|
25
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
26
|
-
import { useTheme } from '../theme/use-theme';
|
|
7
|
+
BottomSheetBase,
|
|
8
|
+
type BottomSheetProps,
|
|
9
|
+
type BottomSheetRef,
|
|
10
|
+
type BottomSheetShellProps,
|
|
11
|
+
} from './BottomSheetBase';
|
|
27
12
|
|
|
28
13
|
// Keyboard handling — only on native platforms. On web, keyboard events are
|
|
29
|
-
// handled by the browser
|
|
14
|
+
// handled by the browser (and the web build resolves to `index.web.tsx`, which
|
|
15
|
+
// never imports this module). `react-native-keyboard-controller` is an OPTIONAL
|
|
30
16
|
// dependency: it is lazily `require`d below and everything no-ops when it is
|
|
31
17
|
// absent (or on web).
|
|
32
18
|
const noopKeyboardHandler = (_handlers: Record<string, (e: { height: number }) => void>, _deps: unknown[]) => {};
|
|
@@ -83,696 +69,26 @@ function SheetKeyboardSync({ keyboardHeight }: { keyboardHeight: SharedValue<num
|
|
|
83
69
|
return null;
|
|
84
70
|
}
|
|
85
71
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
});
|
|
94
|
-
return () => subscription.remove();
|
|
95
|
-
}, []);
|
|
96
|
-
|
|
97
|
-
return dimensions;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const SPRING_CONFIG = {
|
|
101
|
-
damping: 25,
|
|
102
|
-
stiffness: 300,
|
|
103
|
-
mass: 0.8,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export interface BottomSheetRef {
|
|
107
|
-
present: () => void;
|
|
108
|
-
dismiss: () => void;
|
|
109
|
-
close: () => void;
|
|
110
|
-
expand: () => void;
|
|
111
|
-
collapse: () => void;
|
|
112
|
-
scrollTo: (y: number, animated?: boolean) => void;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface BottomSheetProps {
|
|
116
|
-
children: React.ReactNode;
|
|
117
|
-
onDismiss?: () => void;
|
|
118
|
-
enablePanDownToClose?: boolean;
|
|
119
|
-
backgroundComponent?: (props: { style?: StyleProp<ViewStyle> }) => React.ReactElement | null;
|
|
120
|
-
backdropComponent?: (props: { style?: StyleProp<ViewStyle>; onPress?: () => void }) => React.ReactElement | null;
|
|
121
|
-
/**
|
|
122
|
-
* Style applied to the sheet container (the outer Animated.View positioned at
|
|
123
|
-
* the bottom of the screen). Use this to override `maxWidth`, `height`,
|
|
124
|
-
* background color, border radius, etc. Composed AFTER the internal sheet
|
|
125
|
-
* styles so it can override them.
|
|
126
|
-
*/
|
|
127
|
-
style?: StyleProp<ViewStyle>;
|
|
128
|
-
enableHandlePanningGesture?: boolean;
|
|
129
|
-
onDismissAttempt?: () => boolean;
|
|
130
|
-
detached?: boolean; // If true, shows with margins and rounded corners. If false, full width with rounded top only.
|
|
131
|
-
/**
|
|
132
|
-
* Whether to render the built-in (non-interactive) drag handle bar at the top
|
|
133
|
-
* of the sheet. Defaults to `true`. Set to `false` when the consumer renders
|
|
134
|
-
* its own handle (e.g. an interactive close affordance) inside `children`.
|
|
135
|
-
*/
|
|
136
|
-
showHandle?: boolean;
|
|
137
|
-
/**
|
|
138
|
-
* Opacity of the dimming backdrop behind the sheet (0–1). Defaults to `0.5`.
|
|
139
|
-
* Set to a higher value (e.g. `0.7`) when the sheet is presented over another
|
|
140
|
-
* bottom sheet (Dialog cases) so the underlying handle/content does not
|
|
141
|
-
* bleed through.
|
|
142
|
-
*/
|
|
143
|
-
backdropOpacity?: number;
|
|
144
|
-
/**
|
|
145
|
-
* When `true` (default), children are wrapped in an internal scrollable
|
|
146
|
-
* container — convenient for vertical content that can overflow.
|
|
147
|
-
*
|
|
148
|
-
* Set to `false` when the screen owns its own scrolling primitive
|
|
149
|
-
* (e.g. a `FlatList`, `SectionList`, or any other VirtualizedList).
|
|
150
|
-
* Nesting a VirtualizedList inside the internal ScrollView would break
|
|
151
|
-
* windowing/keyboard handling and trigger a React Native warning. In
|
|
152
|
-
* non-scrollable mode the screen receives the full available height
|
|
153
|
-
* (minus the drag handle) and must manage its own overflow.
|
|
154
|
-
*/
|
|
155
|
-
scrollable?: boolean;
|
|
156
|
-
/**
|
|
157
|
-
* When `true`, the body pan uses RNGH's `manualActivation` and only
|
|
158
|
-
* activates when (a) the inner ScrollView is at the top AND (b) the user
|
|
159
|
-
* has moved their finger downward by > 8dp. This is the @gorhom/bottom-sheet
|
|
160
|
-
* coordination model — recommended for sheets containing scrollable content
|
|
161
|
-
* on Android (avoids stealing vertical events from the inner scroller).
|
|
162
|
-
*
|
|
163
|
-
* When `false` (default), the body pan is always active and gates on the
|
|
164
|
-
* scroll offset at `onStart` time. This is the legacy behavior, preserved
|
|
165
|
-
* for backwards compatibility with current bloom consumers.
|
|
166
|
-
*
|
|
167
|
-
* Enabling this also splits the drag handle into its own dedicated,
|
|
168
|
-
* unconditional pan so users can always grab the handle to drag — even
|
|
169
|
-
* when the inner ScrollView is mid-scroll.
|
|
170
|
-
*/
|
|
171
|
-
manualActivation?: boolean;
|
|
172
|
-
/**
|
|
173
|
-
* When `true`, the backdrop dims proportionally with drag distance — the
|
|
174
|
-
* overlay fades from full opacity (sheet at rest) to 30% as the sheet is
|
|
175
|
-
* pulled down 40% of the screen height. iOS Photos style. The base
|
|
176
|
-
* `backdropOpacity` still controls the resting dim level.
|
|
177
|
-
*
|
|
178
|
-
* Defaults to `false` (constant opacity during drag).
|
|
179
|
-
*/
|
|
180
|
-
dynamicBackdrop?: boolean;
|
|
181
|
-
/**
|
|
182
|
-
* Custom handle slot. When provided, replaces the default drag handle
|
|
183
|
-
* (the 36×5 pill). The rendered handle is wrapped in the dedicated handle
|
|
184
|
-
* gesture detector (when `manualActivation` is `true`) so it remains
|
|
185
|
-
* unconditionally draggable. `showHandle={false}` still suppresses any
|
|
186
|
-
* handle rendering — `handleComponent` is only consulted when
|
|
187
|
-
* `showHandle` is `true`.
|
|
188
|
-
*/
|
|
189
|
-
handleComponent?: () => React.ReactNode;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef<BottomSheetRef>) => {
|
|
193
|
-
const {
|
|
194
|
-
children,
|
|
195
|
-
onDismiss,
|
|
196
|
-
enablePanDownToClose = true,
|
|
197
|
-
backgroundComponent,
|
|
198
|
-
backdropComponent,
|
|
199
|
-
style,
|
|
200
|
-
enableHandlePanningGesture = true,
|
|
201
|
-
onDismissAttempt,
|
|
202
|
-
detached = false,
|
|
203
|
-
showHandle = true,
|
|
204
|
-
backdropOpacity = 0.5,
|
|
205
|
-
scrollable = true,
|
|
206
|
-
manualActivation = false,
|
|
207
|
-
dynamicBackdrop = false,
|
|
208
|
-
handleComponent,
|
|
209
|
-
} = props;
|
|
210
|
-
|
|
211
|
-
const insets = useSafeAreaInsets();
|
|
212
|
-
const theme = useTheme();
|
|
213
|
-
const { colors } = theme;
|
|
214
|
-
const { height: screenHeight } = useScreenDimensions();
|
|
215
|
-
const [visible, setVisible] = useState(false);
|
|
216
|
-
const [rendered, setRendered] = useState(false); // keep mounted for exit animation
|
|
217
|
-
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
218
|
-
const hasClosedRef = useRef(false);
|
|
219
|
-
const scrollViewRef = useRef<Animated.ScrollView>(null);
|
|
220
|
-
/**
|
|
221
|
-
* Monotonically increasing counter that identifies "the current close
|
|
222
|
-
* attempt". Bumped every time the sheet re-opens (`present()`), so any
|
|
223
|
-
* in-flight `withTiming` completion callback or fallback timer from a
|
|
224
|
-
* PREVIOUS close cycle becomes a no-op. Without this guard, a stale
|
|
225
|
-
* `runOnJS(finishClose)` from an aborted close would fire `onDismiss`
|
|
226
|
-
* and unmount the sheet immediately after the user opens it again,
|
|
227
|
-
* causing "tap to open does nothing" reports in production.
|
|
228
|
-
*/
|
|
229
|
-
const closeGenerationRef = useRef(0);
|
|
230
|
-
|
|
231
|
-
const screenHeightSV = useSharedValue(screenHeight);
|
|
232
|
-
// Keep shared value in sync when screen dimensions change (rotation/resize)
|
|
233
|
-
useEffect(() => {
|
|
234
|
-
screenHeightSV.value = screenHeight;
|
|
235
|
-
}, [screenHeight, screenHeightSV]);
|
|
236
|
-
|
|
237
|
-
const translateY = useSharedValue(screenHeight);
|
|
238
|
-
const opacity = useSharedValue(0);
|
|
239
|
-
const scrollOffsetY = useSharedValue(0);
|
|
240
|
-
const isScrollAtTop = useSharedValue(true);
|
|
241
|
-
const allowPanClose = useSharedValue(true);
|
|
242
|
-
const keyboardHeight = useSharedValue(0);
|
|
243
|
-
// Stores the sheet's `translateY` captured at the moment a pan gesture
|
|
244
|
-
// begins, so `onUpdate` can offset from the starting position. This MUST
|
|
245
|
-
// be a primitive shared value, not an object-valued one. Under
|
|
246
|
-
// react-native-worklets 0.8.x (Reanimated 4), assigning a fresh object
|
|
247
|
-
// literal to an object-valued shared value's `.value` inside a worklet
|
|
248
|
-
// freezes the object (`freezeObjectInDev`) and routes the mutation through
|
|
249
|
-
// the serializer's listener machinery — which then tries to call the
|
|
250
|
-
// non-worklet `removeListener` synchronously on the UI thread and crashes
|
|
251
|
-
// ("Tried to synchronously call a non-worklet function `removeListener`").
|
|
252
|
-
// A scalar shared value sidesteps all object serialization.
|
|
253
|
-
const contextY = useSharedValue(0);
|
|
254
|
-
// Mirror of `closeGenerationRef` for worklet access. Bumped from the JS
|
|
255
|
-
// thread in lockstep with the ref so gesture worklets always see the
|
|
256
|
-
// current generation when they snapshot it on `onEnd`.
|
|
257
|
-
const closeGeneration = useSharedValue(0);
|
|
258
|
-
// Used by `manualActivation` body pan to track the touch's initial Y so
|
|
259
|
-
// it can compute downward distance for the activation decision.
|
|
260
|
-
const touchStartY = useSharedValue(0);
|
|
261
|
-
|
|
262
|
-
// Refs used to mark the handle pan and the body pan as mutually
|
|
263
|
-
// simultaneous (manualActivation mode only). Without this RNGH treats them
|
|
264
|
-
// as racing gestures and a touch that begins in the handle area could be
|
|
265
|
-
// claimed by whichever recognizer activates first — leading to
|
|
266
|
-
// inconsistent drag start.
|
|
267
|
-
const bodyPanRef = useRef<GestureType | undefined>(undefined);
|
|
268
|
-
const handlePanRef = useRef<GestureType | undefined>(undefined);
|
|
269
|
-
|
|
270
|
-
// The sheet's keyboard-height tracker is registered by <SheetKeyboardSync>,
|
|
271
|
-
// rendered INSIDE the Modal under the re-established <KeyboardProvider> (see
|
|
272
|
-
// the render tree below). It must NOT be called here in the component body:
|
|
273
|
-
// this body runs OUTSIDE the Modal, where the keyboard-controller provider
|
|
274
|
-
// that actually receives the Modal window's insets is not reachable.
|
|
275
|
-
|
|
276
|
-
// Dismiss callbacks
|
|
277
|
-
const safeClose = useCallback(() => {
|
|
278
|
-
if (onDismissAttempt?.()) {
|
|
279
|
-
onDismiss?.();
|
|
280
|
-
} else if (!onDismissAttempt) {
|
|
281
|
-
onDismiss?.();
|
|
282
|
-
}
|
|
283
|
-
}, [onDismissAttempt, onDismiss]);
|
|
284
|
-
|
|
285
|
-
// Mirror `safeClose` and `rendered` into refs so the unmount cleanup can
|
|
286
|
-
// fire the latest dismiss callback when needed, without re-binding the
|
|
287
|
-
// cleanup effect on every render.
|
|
288
|
-
const safeCloseRef = useRef(safeClose);
|
|
289
|
-
useEffect(() => {
|
|
290
|
-
safeCloseRef.current = safeClose;
|
|
291
|
-
}, [safeClose]);
|
|
292
|
-
const renderedRef = useRef(rendered);
|
|
293
|
-
useEffect(() => {
|
|
294
|
-
renderedRef.current = rendered;
|
|
295
|
-
}, [rendered]);
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Commit a close. Two guards prevent stale callbacks from firing:
|
|
299
|
-
* 1. `hasClosedRef` — protects against the fallback timer AND the
|
|
300
|
-
* animation callback both racing to call us within a single close
|
|
301
|
-
* cycle.
|
|
302
|
-
* 2. `generation` — protects against a callback from a PREVIOUS close
|
|
303
|
-
* cycle firing AFTER the user reopened. If the live generation has
|
|
304
|
-
* advanced past the one captured when the close started, this
|
|
305
|
-
* callback is from a cycle that the user has implicitly cancelled
|
|
306
|
-
* by reopening — silently drop it.
|
|
307
|
-
*/
|
|
308
|
-
const finishClose = useCallback((generation: number) => {
|
|
309
|
-
if (closeGenerationRef.current !== generation) return;
|
|
310
|
-
if (hasClosedRef.current) return;
|
|
311
|
-
hasClosedRef.current = true;
|
|
312
|
-
safeClose();
|
|
313
|
-
setRendered(false);
|
|
314
|
-
}, [safeClose]);
|
|
315
|
-
|
|
316
|
-
useEffect(() => {
|
|
317
|
-
if (visible) {
|
|
318
|
-
if (closeTimeoutRef.current) {
|
|
319
|
-
clearTimeout(closeTimeoutRef.current);
|
|
320
|
-
closeTimeoutRef.current = null;
|
|
321
|
-
}
|
|
322
|
-
hasClosedRef.current = false;
|
|
323
|
-
// Bump generation: any pending close-completion callback from a
|
|
324
|
-
// prior cycle (animation or fallback timer) will now no-op when
|
|
325
|
-
// it eventually fires, because its captured generation is stale.
|
|
326
|
-
closeGenerationRef.current += 1;
|
|
327
|
-
closeGeneration.value = closeGenerationRef.current;
|
|
328
|
-
opacity.value = withTiming(1, { duration: 250 });
|
|
329
|
-
translateY.value = withSpring(0, SPRING_CONFIG);
|
|
330
|
-
} else if (rendered) {
|
|
331
|
-
// Capture the generation for THIS close cycle so the animation
|
|
332
|
-
// callback (running on the UI thread, scheduled back to JS) and
|
|
333
|
-
// the fallback timer agree on which cycle they belong to.
|
|
334
|
-
const generation = closeGenerationRef.current;
|
|
335
|
-
opacity.value = withTiming(0, { duration: 250 }, (finished) => {
|
|
336
|
-
if (finished) {
|
|
337
|
-
runOnJS(finishClose)(generation);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
translateY.value = withSpring(screenHeight, { ...SPRING_CONFIG, stiffness: 250 });
|
|
341
|
-
|
|
342
|
-
// Fallback timer to ensure close completes (especially on web
|
|
343
|
-
// where reanimated callbacks occasionally drop on tab blur).
|
|
344
|
-
if (closeTimeoutRef.current) {
|
|
345
|
-
clearTimeout(closeTimeoutRef.current);
|
|
346
|
-
}
|
|
347
|
-
closeTimeoutRef.current = setTimeout(() => {
|
|
348
|
-
finishClose(generation);
|
|
349
|
-
closeTimeoutRef.current = null;
|
|
350
|
-
}, 300);
|
|
351
|
-
}
|
|
352
|
-
}, [visible, rendered, finishClose, screenHeight, closeGeneration, opacity, translateY]);
|
|
353
|
-
|
|
354
|
-
// On unmount: ensure pending close callbacks (e.g. consumer's `onDismiss`)
|
|
355
|
-
// still fire if the BS is yanked mid-animation by a parent re-render.
|
|
356
|
-
// Without this, `Dialog`'s `handleDismiss` never runs and queued
|
|
357
|
-
// callbacks (post-close handlers) are silently lost.
|
|
358
|
-
// Only fires when the sheet was actually rendered (open or closing) to
|
|
359
|
-
// avoid spuriously calling onDismiss on bare unmount of a never-opened
|
|
360
|
-
// sheet. Refs are read inside the cleanup, so latest values are captured.
|
|
361
|
-
useEffect(() => () => {
|
|
362
|
-
if (closeTimeoutRef.current) {
|
|
363
|
-
clearTimeout(closeTimeoutRef.current);
|
|
364
|
-
closeTimeoutRef.current = null;
|
|
365
|
-
}
|
|
366
|
-
if (renderedRef.current && !hasClosedRef.current) {
|
|
367
|
-
hasClosedRef.current = true;
|
|
368
|
-
safeCloseRef.current();
|
|
369
|
-
}
|
|
370
|
-
}, []);
|
|
371
|
-
|
|
372
|
-
// Apply web scrollbar styles when colors change; clean up on unmount
|
|
373
|
-
useEffect(() => {
|
|
374
|
-
if (Platform.OS === 'web') {
|
|
375
|
-
createWebScrollbarStyle(colors.border);
|
|
376
|
-
}
|
|
377
|
-
return () => {
|
|
378
|
-
removeWebScrollbarStyle();
|
|
379
|
-
};
|
|
380
|
-
}, [colors.border]);
|
|
381
|
-
|
|
382
|
-
const present = useCallback(() => {
|
|
383
|
-
setRendered(true);
|
|
384
|
-
setVisible(true);
|
|
385
|
-
}, []);
|
|
386
|
-
const dismiss = useCallback(() => {
|
|
387
|
-
setVisible(false);
|
|
388
|
-
}, []);
|
|
389
|
-
|
|
390
|
-
const scrollTo = useCallback((y: number, animated = true) => {
|
|
391
|
-
scrollViewRef.current?.scrollTo({ y, animated });
|
|
392
|
-
}, []);
|
|
393
|
-
|
|
394
|
-
useImperativeHandle(ref, () => ({
|
|
395
|
-
present,
|
|
396
|
-
dismiss,
|
|
397
|
-
close: dismiss,
|
|
398
|
-
expand: present,
|
|
399
|
-
collapse: dismiss,
|
|
400
|
-
scrollTo,
|
|
401
|
-
}), [present, dismiss, scrollTo]);
|
|
402
|
-
|
|
403
|
-
const nativeGesture = useMemo(() => Gesture.Native(), []);
|
|
404
|
-
|
|
405
|
-
// Body pan — two strategies, switched by `manualActivation`.
|
|
406
|
-
//
|
|
407
|
-
// (1) Legacy mode (`manualActivation: false`, the bloom 0.3.x default):
|
|
408
|
-
// Pan is always active. We snapshot the scroll offset at `onStart`
|
|
409
|
-
// and gate movement on `scrollOffsetY <= 8` during the gesture.
|
|
410
|
-
// This is what current bloom consumers expect.
|
|
411
|
-
//
|
|
412
|
-
// (2) gorhom-style mode (`manualActivation: true`):
|
|
413
|
-
// Pan uses `manualActivation(true)` and only flips to active when
|
|
414
|
-
// the inner ScrollView is at the top AND the user has moved their
|
|
415
|
-
// finger downward > 8dp. In every other case it fails, so the
|
|
416
|
-
// ScrollView keeps full ownership of the touch. This is the only
|
|
417
|
-
// RNGH 2.x pattern that does not steal vertical events from the
|
|
418
|
-
// inner scroller on Android. Required for FileManagement /
|
|
419
|
-
// PhotoPicker style sheets.
|
|
420
|
-
const panGesture = useMemo(() => {
|
|
421
|
-
if (manualActivation) {
|
|
422
|
-
return Gesture.Pan()
|
|
423
|
-
.enabled(enablePanDownToClose)
|
|
424
|
-
.withRef(bodyPanRef)
|
|
425
|
-
.manualActivation(true)
|
|
426
|
-
.simultaneousWithExternalGesture(scrollViewRef, handlePanRef)
|
|
427
|
-
.onTouchesDown((e) => {
|
|
428
|
-
'worklet';
|
|
429
|
-
const t = e.changedTouches[0];
|
|
430
|
-
if (t) touchStartY.value = t.absoluteY;
|
|
431
|
-
contextY.value = translateY.value;
|
|
432
|
-
})
|
|
433
|
-
.onTouchesMove((e, state) => {
|
|
434
|
-
'worklet';
|
|
435
|
-
const t = e.changedTouches[0];
|
|
436
|
-
if (!t) return;
|
|
437
|
-
const dy = t.absoluteY - touchStartY.value;
|
|
438
|
-
const atTop = scrollOffsetY.value <= 4;
|
|
439
|
-
// Activate only when (at scroll top) AND (finger has moved
|
|
440
|
-
// downward by > 8dp). Any other motion: fail so the
|
|
441
|
-
// ScrollView claims the gesture.
|
|
442
|
-
if (atTop && dy > 8) {
|
|
443
|
-
state.activate();
|
|
444
|
-
} else if (dy < -4 || !atTop) {
|
|
445
|
-
state.fail();
|
|
446
|
-
}
|
|
447
|
-
})
|
|
448
|
-
.onUpdate((event) => {
|
|
449
|
-
'worklet';
|
|
450
|
-
if (event.translationY < 0) return;
|
|
451
|
-
const newTranslateY = contextY.value + event.translationY;
|
|
452
|
-
if (newTranslateY >= 0) {
|
|
453
|
-
translateY.value = newTranslateY;
|
|
454
|
-
}
|
|
455
|
-
})
|
|
456
|
-
.onEnd((event) => {
|
|
457
|
-
'worklet';
|
|
458
|
-
const velocity = event.velocityY;
|
|
459
|
-
const distance = translateY.value;
|
|
460
|
-
const closeThreshold = Math.max(140, screenHeightSV.value * 0.25);
|
|
461
|
-
const fastSwipeThreshold = 900;
|
|
462
|
-
const shouldClose =
|
|
463
|
-
velocity > fastSwipeThreshold ||
|
|
464
|
-
(distance > closeThreshold && velocity > -300);
|
|
465
|
-
|
|
466
|
-
if (shouldClose) {
|
|
467
|
-
// Snapshot the generation on the UI thread at the
|
|
468
|
-
// moment the close gesture commits. The completion
|
|
469
|
-
// callback only fires `finishClose` if no reopen
|
|
470
|
-
// bumped the generation in between.
|
|
471
|
-
const generation = closeGeneration.value;
|
|
472
|
-
translateY.value = withSpring(screenHeightSV.value, { ...SPRING_CONFIG, velocity });
|
|
473
|
-
opacity.value = withTiming(0, { duration: 250 }, (finished) => {
|
|
474
|
-
if (finished) runOnJS(finishClose)(generation);
|
|
475
|
-
});
|
|
476
|
-
} else {
|
|
477
|
-
translateY.value = withSpring(0, { ...SPRING_CONFIG, velocity });
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Legacy always-active pan (bloom 0.3.x behaviour).
|
|
483
|
-
return Gesture.Pan()
|
|
484
|
-
.enabled(enablePanDownToClose)
|
|
485
|
-
.simultaneousWithExternalGesture(nativeGesture)
|
|
486
|
-
.onStart(() => {
|
|
487
|
-
'worklet';
|
|
488
|
-
contextY.value = translateY.value;
|
|
489
|
-
allowPanClose.value = scrollOffsetY.value <= 8;
|
|
490
|
-
})
|
|
491
|
-
.onUpdate((event) => {
|
|
492
|
-
'worklet';
|
|
493
|
-
if (!allowPanClose.value) {
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
const newTranslateY = contextY.value + event.translationY;
|
|
497
|
-
// If user is scrolling down while content isn't at (or near) the top, let ScrollView handle it
|
|
498
|
-
const atTopOrNearTop = scrollOffsetY.value <= 8; // slightly larger tolerance for smoother handoff
|
|
499
|
-
if (event.translationY > 0 && !atTopOrNearTop) {
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
if (newTranslateY >= 0) {
|
|
503
|
-
translateY.value = newTranslateY;
|
|
504
|
-
} else if (detached) {
|
|
505
|
-
// Only allow overdrag (pulling up beyond top) when detached
|
|
506
|
-
translateY.value = newTranslateY * 0.3;
|
|
507
|
-
} else {
|
|
508
|
-
// In normal mode, prevent overdrag - clamp to 0
|
|
509
|
-
translateY.value = 0;
|
|
510
|
-
}
|
|
511
|
-
})
|
|
512
|
-
.onEnd((event) => {
|
|
513
|
-
'worklet';
|
|
514
|
-
if (!allowPanClose.value) {
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
const velocity = event.velocityY;
|
|
518
|
-
const distance = translateY.value;
|
|
519
|
-
// Require a deeper pull to close (more like native bottom sheets)
|
|
520
|
-
const closeThreshold = Math.max(140, screenHeightSV.value * 0.25);
|
|
521
|
-
const fastSwipeThreshold = 900;
|
|
522
|
-
const shouldClose =
|
|
523
|
-
velocity > fastSwipeThreshold ||
|
|
524
|
-
(distance > closeThreshold && velocity > -300);
|
|
525
|
-
|
|
526
|
-
if (shouldClose) {
|
|
527
|
-
const generation = closeGeneration.value;
|
|
528
|
-
translateY.value = withSpring(screenHeightSV.value, {
|
|
529
|
-
...SPRING_CONFIG,
|
|
530
|
-
velocity: velocity,
|
|
531
|
-
});
|
|
532
|
-
opacity.value = withTiming(0, { duration: 250 }, (finished) => {
|
|
533
|
-
if (finished) {
|
|
534
|
-
runOnJS(finishClose)(generation);
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
} else {
|
|
538
|
-
translateY.value = withSpring(0, {
|
|
539
|
-
...SPRING_CONFIG,
|
|
540
|
-
velocity: velocity,
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
// Shared values are stable refs; the listed deps are the only JS-side
|
|
545
|
-
// values that change the gesture's behavior. `finishClose` is stable
|
|
546
|
-
// (useCallback with stable deps).
|
|
547
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
548
|
-
}, [enablePanDownToClose, detached, manualActivation, nativeGesture, finishClose]);
|
|
549
|
-
|
|
550
|
-
// Dedicated handle pan — only built in `manualActivation` mode. Always
|
|
551
|
-
// active so users can drag the handle even while content is mid-scroll.
|
|
552
|
-
// In legacy mode the body pan already wraps the whole sheet (handle
|
|
553
|
-
// included), so no separate gesture is needed.
|
|
554
|
-
const handlePanGesture = useMemo(() => {
|
|
555
|
-
if (!manualActivation) return undefined;
|
|
556
|
-
return Gesture.Pan()
|
|
557
|
-
.enabled(enablePanDownToClose && enableHandlePanningGesture)
|
|
558
|
-
.withRef(handlePanRef)
|
|
559
|
-
.simultaneousWithExternalGesture(bodyPanRef)
|
|
560
|
-
.activeOffsetY([-8, 8])
|
|
561
|
-
.onStart(() => {
|
|
562
|
-
'worklet';
|
|
563
|
-
contextY.value = translateY.value;
|
|
564
|
-
})
|
|
565
|
-
.onUpdate((event) => {
|
|
566
|
-
'worklet';
|
|
567
|
-
const newTranslateY = contextY.value + event.translationY;
|
|
568
|
-
if (newTranslateY >= 0) {
|
|
569
|
-
translateY.value = newTranslateY;
|
|
570
|
-
} else if (detached) {
|
|
571
|
-
translateY.value = newTranslateY * 0.3;
|
|
572
|
-
} else {
|
|
573
|
-
translateY.value = 0;
|
|
574
|
-
}
|
|
575
|
-
})
|
|
576
|
-
.onEnd((event) => {
|
|
577
|
-
'worklet';
|
|
578
|
-
const velocity = event.velocityY;
|
|
579
|
-
const distance = translateY.value;
|
|
580
|
-
const closeThreshold = Math.max(140, screenHeightSV.value * 0.25);
|
|
581
|
-
const fastSwipeThreshold = 900;
|
|
582
|
-
const shouldClose =
|
|
583
|
-
velocity > fastSwipeThreshold ||
|
|
584
|
-
(distance > closeThreshold && velocity > -300);
|
|
585
|
-
|
|
586
|
-
if (shouldClose) {
|
|
587
|
-
const generation = closeGeneration.value;
|
|
588
|
-
translateY.value = withSpring(screenHeightSV.value, { ...SPRING_CONFIG, velocity });
|
|
589
|
-
opacity.value = withTiming(0, { duration: 250 }, (finished) => {
|
|
590
|
-
if (finished) runOnJS(finishClose)(generation);
|
|
591
|
-
});
|
|
592
|
-
} else {
|
|
593
|
-
translateY.value = withSpring(0, { ...SPRING_CONFIG, velocity });
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
597
|
-
}, [manualActivation, enablePanDownToClose, enableHandlePanningGesture, detached, finishClose]);
|
|
598
|
-
|
|
599
|
-
// `opacity.value` drives the fade in/out (0 -> 1). `backdropOpacity` is the
|
|
600
|
-
// final dim level once fully visible. We multiply so the consumer-provided
|
|
601
|
-
// dim opacity applies smoothly across the animation. When
|
|
602
|
-
// `dynamicBackdrop` is enabled, the dim also fades proportionally with
|
|
603
|
-
// drag distance (iOS Photos style).
|
|
604
|
-
const backdropStyle = useAnimatedStyle(() => {
|
|
605
|
-
const dragFactor = dynamicBackdrop
|
|
606
|
-
? interpolate(
|
|
607
|
-
translateY.value,
|
|
608
|
-
[0, screenHeightSV.value * 0.4],
|
|
609
|
-
[1, 0.3],
|
|
610
|
-
'clamp',
|
|
611
|
-
)
|
|
612
|
-
: 1;
|
|
613
|
-
return {
|
|
614
|
-
opacity: opacity.value * backdropOpacity * dragFactor,
|
|
615
|
-
};
|
|
616
|
-
}, [backdropOpacity, dynamicBackdrop]);
|
|
617
|
-
|
|
618
|
-
const sheetStyle = useAnimatedStyle(() => {
|
|
619
|
-
const scale = interpolate(translateY.value, [0, screenHeightSV.value], [1, 0.95]);
|
|
620
|
-
return {
|
|
621
|
-
transform: [
|
|
622
|
-
{ translateY: translateY.value - keyboardHeight.value },
|
|
623
|
-
{ scale },
|
|
624
|
-
],
|
|
625
|
-
};
|
|
626
|
-
}, []);
|
|
627
|
-
|
|
628
|
-
const sheetHeightStyle = useAnimatedStyle(() => ({
|
|
629
|
-
maxHeight: screenHeightSV.value - keyboardHeight.value - insets.top - (detached ? insets.bottom + 16 : 0),
|
|
630
|
-
}), [insets.top, insets.bottom, detached]);
|
|
631
|
-
|
|
632
|
-
const sheetMarginStyle = useAnimatedStyle(() => {
|
|
633
|
-
// Only add margin when detached, otherwise extend behind safe area
|
|
634
|
-
if (detached) {
|
|
635
|
-
return {
|
|
636
|
-
marginBottom: keyboardHeight.value > 0 ? 16 : insets.bottom + 16,
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
return {
|
|
640
|
-
marginBottom: 0,
|
|
641
|
-
};
|
|
642
|
-
}, [insets.bottom, detached]);
|
|
643
|
-
|
|
644
|
-
const handleBackdropPress = useCallback(() => {
|
|
645
|
-
// Always animate close on backdrop press
|
|
646
|
-
if (onDismissAttempt && !onDismissAttempt()) {
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
dismiss();
|
|
650
|
-
}, [onDismissAttempt, dismiss]);
|
|
651
|
-
|
|
652
|
-
const scrollHandler = useAnimatedScrollHandler({
|
|
653
|
-
onScroll: (event) => {
|
|
654
|
-
scrollOffsetY.value = event.contentOffset.y;
|
|
655
|
-
isScrollAtTop.value = event.contentOffset.y <= 0;
|
|
656
|
-
},
|
|
657
|
-
}, []);
|
|
658
|
-
|
|
659
|
-
const dynamicStyles = useMemo(() => {
|
|
660
|
-
return StyleSheet.create({
|
|
661
|
-
handle: {
|
|
662
|
-
...styles.handle,
|
|
663
|
-
backgroundColor: theme.isDark ? '#444' : '#C7C7CC',
|
|
664
|
-
},
|
|
665
|
-
sheet: {
|
|
666
|
-
...styles.sheet,
|
|
667
|
-
backgroundColor: colors.background,
|
|
668
|
-
...(detached ? styles.sheetDetached : styles.sheetNormal),
|
|
669
|
-
},
|
|
670
|
-
scrollContent: {
|
|
671
|
-
...styles.scrollContent,
|
|
672
|
-
// In normal mode, don't add padding here - screens handle their own padding
|
|
673
|
-
// The sheet extends behind safe area, and screens add padding as needed
|
|
674
|
-
},
|
|
675
|
-
});
|
|
676
|
-
}, [colors.background, theme.isDark, detached]);
|
|
677
|
-
|
|
678
|
-
if (!rendered) return null;
|
|
679
|
-
|
|
680
|
-
// Default handle render — used when `handleComponent` is not provided.
|
|
681
|
-
const renderDefaultHandle = () => <View style={dynamicStyles.handle} />;
|
|
682
|
-
const handleNode = showHandle ? (handleComponent ? handleComponent() : renderDefaultHandle()) : null;
|
|
683
|
-
|
|
684
|
-
// In `manualActivation` mode the handle gets its own gesture detector
|
|
685
|
-
// sitting in a dedicated absolutely-positioned hit area at the top of the
|
|
686
|
-
// sheet. In legacy mode the handle is rendered inline as a decorative
|
|
687
|
-
// overlay (the body pan covers the entire sheet, handle included).
|
|
688
|
-
const handleSlot = handleNode && manualActivation && handlePanGesture ? (
|
|
689
|
-
<GestureDetector gesture={handlePanGesture}>
|
|
690
|
-
<View style={styles.handleHitArea} accessible accessibilityRole="adjustable">
|
|
691
|
-
{handleNode}
|
|
692
|
-
</View>
|
|
693
|
-
</GestureDetector>
|
|
694
|
-
) : handleNode;
|
|
695
|
-
|
|
696
|
-
// Inner content: scrollable wraps in Animated.ScrollView, non-scrollable
|
|
697
|
-
// renders children directly. In legacy mode the scrollview is also
|
|
698
|
-
// wrapped in the `nativeGesture` detector for scroll/pan coordination.
|
|
699
|
-
const scrollViewNode = (
|
|
700
|
-
<Animated.ScrollView
|
|
701
|
-
ref={scrollViewRef}
|
|
702
|
-
style={[
|
|
703
|
-
styles.scrollView,
|
|
704
|
-
Platform.OS === 'web' && ({
|
|
705
|
-
scrollbarWidth: 'thin',
|
|
706
|
-
scrollbarColor: `${colors.border} transparent`,
|
|
707
|
-
} as ViewStyle),
|
|
708
|
-
]}
|
|
709
|
-
contentContainerStyle={dynamicStyles.scrollContent}
|
|
710
|
-
showsVerticalScrollIndicator={false}
|
|
711
|
-
keyboardShouldPersistTaps="handled"
|
|
712
|
-
onScroll={scrollHandler}
|
|
713
|
-
scrollEventThrottle={16}
|
|
714
|
-
{...(Platform.OS === 'web' ? { className: 'bottom-sheet-scrollview' } : undefined)}
|
|
715
|
-
onLayout={() => {
|
|
716
|
-
if (Platform.OS === 'web') {
|
|
717
|
-
createWebScrollbarStyle(colors.border);
|
|
718
|
-
}
|
|
719
|
-
}}
|
|
720
|
-
>
|
|
721
|
-
{children}
|
|
722
|
-
</Animated.ScrollView>
|
|
723
|
-
);
|
|
724
|
-
|
|
725
|
-
const bodyContent = scrollable
|
|
726
|
-
? (manualActivation
|
|
727
|
-
// In manualActivation mode the scroll view is referenced by the
|
|
728
|
-
// pan's simultaneous list directly; no wrapping native gesture.
|
|
729
|
-
? scrollViewNode
|
|
730
|
-
// Legacy mode: native gesture wraps the scroll view to coordinate
|
|
731
|
-
// with the always-active body pan.
|
|
732
|
-
: <GestureDetector gesture={nativeGesture}>{scrollViewNode}</GestureDetector>)
|
|
733
|
-
: <View style={styles.nonScrollableContent}>{children}</View>;
|
|
734
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Native shell: RN's `<Modal>` (its own native root window) + the
|
|
74
|
+
* re-established `<KeyboardProvider>` + the sheet's keyboard tracker + a
|
|
75
|
+
* `<GestureHandlerRootView>` (the app-root GHRV does not cross the Modal's
|
|
76
|
+
* native-window boundary, so pan gestures need their own root here).
|
|
77
|
+
*/
|
|
78
|
+
function NativeShell({ visible, onRequestClose, keyboardHeight, children }: BottomSheetShellProps) {
|
|
735
79
|
return (
|
|
736
|
-
<Modal visible={
|
|
737
|
-
{/* RN's <Modal> mounts into its OWN native root, so the app-root
|
|
738
|
-
react-native-keyboard-controller <KeyboardProvider> does not
|
|
739
|
-
reach this subtree. Re-establish a provider here so the sheet's
|
|
740
|
-
own keyboard tracker (<SheetKeyboardSync>) AND any TextInputs in
|
|
741
|
-
`children` find a real KeyboardContext + receive the Modal
|
|
742
|
-
window's keyboard insets. Resolves to a passthrough on web / when
|
|
743
|
-
the optional module isn't installed (see resolution above). */}
|
|
80
|
+
<Modal visible={visible} transparent animationType="none" statusBarTranslucent onRequestClose={onRequestClose}>
|
|
744
81
|
<KeyboardProvider>
|
|
745
82
|
<SheetKeyboardSync keyboardHeight={keyboardHeight} />
|
|
746
|
-
{
|
|
747
|
-
GestureHandlerRootView does NOT extend into it, so pan
|
|
748
|
-
gestures silently no-op without this wrapper. */}
|
|
749
|
-
<GestureHandlerRootView style={styles.rootView}>
|
|
750
|
-
<View style={StyleSheet.absoluteFill}>
|
|
751
|
-
<Animated.View style={[styles.backdrop, backdropStyle]}>
|
|
752
|
-
{backdropComponent ? (
|
|
753
|
-
backdropComponent({ onPress: handleBackdropPress })
|
|
754
|
-
) : (
|
|
755
|
-
<Pressable style={styles.backdropTouchable} onPress={handleBackdropPress}>
|
|
756
|
-
<View style={StyleSheet.absoluteFill} />
|
|
757
|
-
</Pressable>
|
|
758
|
-
)}
|
|
759
|
-
</Animated.View>
|
|
760
|
-
|
|
761
|
-
<GestureDetector gesture={panGesture}>
|
|
762
|
-
<Animated.View style={[dynamicStyles.sheet, sheetMarginStyle, sheetStyle, sheetHeightStyle, style]}>
|
|
763
|
-
{backgroundComponent?.({ style: styles.background })}
|
|
764
|
-
|
|
765
|
-
{handleSlot}
|
|
766
|
-
|
|
767
|
-
{bodyContent}
|
|
768
|
-
</Animated.View>
|
|
769
|
-
</GestureDetector>
|
|
770
|
-
</View>
|
|
771
|
-
</GestureHandlerRootView>
|
|
83
|
+
<GestureHandlerRootView style={styles.rootView}>{children}</GestureHandlerRootView>
|
|
772
84
|
</KeyboardProvider>
|
|
773
85
|
</Modal>
|
|
774
86
|
);
|
|
775
|
-
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef<BottomSheetRef>) => (
|
|
90
|
+
<BottomSheetBase {...props} ref={ref} Shell={NativeShell} />
|
|
91
|
+
));
|
|
776
92
|
|
|
777
93
|
BottomSheet.displayName = 'BottomSheet';
|
|
778
94
|
|
|
@@ -780,119 +96,8 @@ const styles = StyleSheet.create({
|
|
|
780
96
|
rootView: {
|
|
781
97
|
flex: 1,
|
|
782
98
|
},
|
|
783
|
-
backdrop: {
|
|
784
|
-
// Color is solid black; the dim level is driven by `backdropOpacity` via
|
|
785
|
-
// the animated `opacity` style on the wrapping <Animated.View>.
|
|
786
|
-
flex: 1,
|
|
787
|
-
backgroundColor: '#000',
|
|
788
|
-
},
|
|
789
|
-
backdropTouchable: {
|
|
790
|
-
flex: 1,
|
|
791
|
-
},
|
|
792
|
-
sheet: {
|
|
793
|
-
position: 'absolute',
|
|
794
|
-
bottom: 0,
|
|
795
|
-
overflow: 'hidden',
|
|
796
|
-
maxWidth: 800,
|
|
797
|
-
alignSelf: 'center',
|
|
798
|
-
marginHorizontal: 'auto',
|
|
799
|
-
},
|
|
800
|
-
sheetDetached: {
|
|
801
|
-
left: 16,
|
|
802
|
-
right: 16,
|
|
803
|
-
borderRadius: 24,
|
|
804
|
-
},
|
|
805
|
-
sheetNormal: {
|
|
806
|
-
left: 0,
|
|
807
|
-
right: 0,
|
|
808
|
-
borderTopLeftRadius: 24,
|
|
809
|
-
borderTopRightRadius: 24,
|
|
810
|
-
},
|
|
811
|
-
/** Legacy (non-manualActivation) handle: decorative overlay only. */
|
|
812
|
-
handle: {
|
|
813
|
-
position: 'absolute',
|
|
814
|
-
top: 10,
|
|
815
|
-
left: '50%',
|
|
816
|
-
marginLeft: -18,
|
|
817
|
-
width: 36,
|
|
818
|
-
height: 5,
|
|
819
|
-
borderRadius: 3,
|
|
820
|
-
zIndex: Z_INDEX.sheetHandle,
|
|
821
|
-
},
|
|
822
|
-
/**
|
|
823
|
-
* Hit area for the drag handle in `manualActivation` mode. Absolutely
|
|
824
|
-
* positioned at the top of the sheet so the area visually "floats" above
|
|
825
|
-
* the content — content scrolls up underneath it (no layout offset)
|
|
826
|
-
* while the thumb can still grab the full-width 28dp strip to drag.
|
|
827
|
-
*/
|
|
828
|
-
handleHitArea: {
|
|
829
|
-
position: 'absolute',
|
|
830
|
-
top: 0,
|
|
831
|
-
left: 0,
|
|
832
|
-
right: 0,
|
|
833
|
-
height: 28,
|
|
834
|
-
alignItems: 'center',
|
|
835
|
-
justifyContent: 'flex-start',
|
|
836
|
-
paddingTop: 6,
|
|
837
|
-
zIndex: Z_INDEX.sheetHandle,
|
|
838
|
-
},
|
|
839
|
-
background: {
|
|
840
|
-
...StyleSheet.absoluteFill,
|
|
841
|
-
},
|
|
842
|
-
scrollView: {
|
|
843
|
-
flex: 1,
|
|
844
|
-
},
|
|
845
|
-
scrollContent: {
|
|
846
|
-
flexGrow: 1,
|
|
847
|
-
},
|
|
848
|
-
nonScrollableContent: {
|
|
849
|
-
flex: 1,
|
|
850
|
-
},
|
|
851
99
|
});
|
|
852
100
|
|
|
853
|
-
|
|
854
|
-
const createWebScrollbarStyle = (borderColor: string) => {
|
|
855
|
-
if (Platform.OS !== 'web' || typeof document === 'undefined') return;
|
|
856
|
-
|
|
857
|
-
let styleElement = document.getElementById(SCROLLBAR_STYLE_ID) as HTMLStyleElement | null;
|
|
858
|
-
|
|
859
|
-
if (!styleElement) {
|
|
860
|
-
styleElement = document.createElement('style');
|
|
861
|
-
styleElement.id = SCROLLBAR_STYLE_ID;
|
|
862
|
-
document.head.appendChild(styleElement);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// Derive a slightly darker scrollbar hover color from the border color
|
|
866
|
-
const scrollbarColor = borderColor;
|
|
867
|
-
const scrollbarHoverColor = borderColor.startsWith('hsl')
|
|
868
|
-
? borderColor.replace(/\)$/, ' / 0.7)') // add alpha for hover
|
|
869
|
-
: '#888';
|
|
870
|
-
|
|
871
|
-
styleElement.textContent = `
|
|
872
|
-
.bottom-sheet-scrollview::-webkit-scrollbar {
|
|
873
|
-
width: 6px;
|
|
874
|
-
}
|
|
875
|
-
.bottom-sheet-scrollview::-webkit-scrollbar-track {
|
|
876
|
-
background: transparent;
|
|
877
|
-
border-radius: 10px;
|
|
878
|
-
}
|
|
879
|
-
.bottom-sheet-scrollview::-webkit-scrollbar-thumb {
|
|
880
|
-
background: ${scrollbarColor};
|
|
881
|
-
border-radius: 10px;
|
|
882
|
-
}
|
|
883
|
-
.bottom-sheet-scrollview::-webkit-scrollbar-thumb:hover {
|
|
884
|
-
background: ${scrollbarHoverColor};
|
|
885
|
-
}
|
|
886
|
-
`;
|
|
887
|
-
};
|
|
888
|
-
|
|
889
|
-
const SCROLLBAR_STYLE_ID = 'bottom-sheet-scrollbar-style';
|
|
890
|
-
|
|
891
|
-
/** Remove the injected scrollbar <style> tag on unmount. */
|
|
892
|
-
const removeWebScrollbarStyle = () => {
|
|
893
|
-
if (Platform.OS !== 'web' || typeof document === 'undefined') return;
|
|
894
|
-
document.getElementById(SCROLLBAR_STYLE_ID)?.remove();
|
|
895
|
-
};
|
|
896
|
-
|
|
101
|
+
export type { BottomSheetProps, BottomSheetRef };
|
|
897
102
|
export default BottomSheet;
|
|
898
103
|
export { BottomSheet };
|