@webority-technologies/mobile 0.0.13 → 0.0.15
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/components/BottomSheet/BottomSheet.js +59 -10
- package/lib/module/components/BottomSheet/BottomSheet.js +61 -12
- package/lib/typescript/commonjs/components/BottomSheet/BottomSheet.d.ts +10 -0
- package/lib/typescript/module/components/BottomSheet/BottomSheet.d.ts +10 -0
- package/package.json +1 -1
|
@@ -49,6 +49,7 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
|
|
|
49
49
|
enableBackdropPress = true,
|
|
50
50
|
backdropOpacity = 0.5,
|
|
51
51
|
keyboardBehavior = 'none',
|
|
52
|
+
mode = 'modal',
|
|
52
53
|
handleIndicatorStyle,
|
|
53
54
|
containerStyle,
|
|
54
55
|
children,
|
|
@@ -84,6 +85,10 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
|
|
|
84
85
|
const currentIndexShared = (0, _reactNativeReanimated.useSharedValue)(-1);
|
|
85
86
|
const [currentIndex, setCurrentIndex] = (0, _react.useState)(-1);
|
|
86
87
|
const isAnimatingRef = (0, _react.useRef)(false);
|
|
88
|
+
// Drives the native <Modal>'s visible prop in modal mode. We mount the modal
|
|
89
|
+
// synchronously on open and unmount only after the close animation finishes
|
|
90
|
+
// so the slide-down stays visible.
|
|
91
|
+
const [modalVisible, setModalVisible] = (0, _react.useState)(false);
|
|
87
92
|
|
|
88
93
|
// Convert a snap-point index → translateY position. -1 = closed.
|
|
89
94
|
const yForIndex = (0, _react.useCallback)(idx => {
|
|
@@ -111,14 +116,26 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
|
|
|
111
116
|
}, [onAnimate, translateY]);
|
|
112
117
|
const markAnimationDone = (0, _react.useCallback)(() => {
|
|
113
118
|
isAnimatingRef.current = false;
|
|
114
|
-
|
|
119
|
+
// If we just finished a close animation, unmount the modal wrapper.
|
|
120
|
+
if (mode === 'modal' && currentIndexShared.value < 0) {
|
|
121
|
+
setModalVisible(false);
|
|
122
|
+
}
|
|
123
|
+
}, [mode, currentIndexShared]);
|
|
115
124
|
const expand = (0, _react.useCallback)(idx => {
|
|
116
125
|
const target = typeof idx === 'number' ? clamp(idx, 0, resolvedSnapPoints.length - 1) : currentIndex >= 0 ? currentIndex : 0;
|
|
117
126
|
const fromIndex = currentIndexShared.value;
|
|
118
127
|
const to = yForIndex(target);
|
|
128
|
+
// Mount the Modal before kicking off the spring so the sheet has a
|
|
129
|
+
// host to animate into.
|
|
130
|
+
if (mode === 'modal') setModalVisible(true);
|
|
131
|
+
// Reset translateY to the closed position so the first open animates
|
|
132
|
+
// up from off-screen rather than snapping in place.
|
|
133
|
+
if (fromIndex < 0) {
|
|
134
|
+
translateY.value = closedY;
|
|
135
|
+
}
|
|
119
136
|
setIndexJS(target);
|
|
120
137
|
animateTo(to, fromIndex, target);
|
|
121
|
-
}, [animateTo, currentIndex, currentIndexShared, resolvedSnapPoints.length, setIndexJS, yForIndex]);
|
|
138
|
+
}, [animateTo, closedY, currentIndex, currentIndexShared, mode, resolvedSnapPoints.length, setIndexJS, translateY, yForIndex]);
|
|
122
139
|
const collapse = (0, _react.useCallback)(() => {
|
|
123
140
|
if (resolvedSnapPoints.length === 0) return;
|
|
124
141
|
const fromIndex = currentIndexShared.value;
|
|
@@ -270,10 +287,15 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
|
|
|
270
287
|
}, [keyboardBehavior, keyboardOffset]);
|
|
271
288
|
|
|
272
289
|
// ───────── Animated styles ─────────
|
|
290
|
+
const safeAreaTop = insets.top;
|
|
273
291
|
const sheetStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => {
|
|
274
|
-
// Don't push past the screen top — keyboard offset is clamped to minTopY.
|
|
275
292
|
const yWithKb = translateY.value + keyboardOffset.value;
|
|
276
|
-
|
|
293
|
+
// When the keyboard is up, allow the sheet to slide above its natural
|
|
294
|
+
// max-snap-point top (`minTopY`) so the input area stays visible. We still
|
|
295
|
+
// clamp to the safe-area top so the sheet doesn't disappear under the
|
|
296
|
+
// status bar / notch.
|
|
297
|
+
const lowerBound = keyboardOffset.value < 0 ? safeAreaTop : minTopY;
|
|
298
|
+
const clamped = yWithKb < lowerBound ? lowerBound : yWithKb;
|
|
277
299
|
return {
|
|
278
300
|
transform: [{
|
|
279
301
|
translateY: clamped
|
|
@@ -300,23 +322,31 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
|
|
|
300
322
|
}, [enableBackdropPress, close]);
|
|
301
323
|
|
|
302
324
|
// Don't render the heavy gesture tree at all when nothing's been opened yet.
|
|
303
|
-
//
|
|
304
|
-
//
|
|
325
|
+
// For inline mode we keep the legacy "mount on first open" behavior; modal
|
|
326
|
+
// mode is gated entirely by `modalVisible`.
|
|
305
327
|
const everOpenedRef = (0, _react.useRef)(false);
|
|
306
328
|
if (isExpanded || controlledIndex !== undefined) {
|
|
307
329
|
everOpenedRef.current = true;
|
|
308
330
|
}
|
|
309
|
-
if (!everOpenedRef.current) {
|
|
331
|
+
if (mode === 'inline' && !everOpenedRef.current) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
if (mode === 'modal' && !modalVisible) {
|
|
310
335
|
return null;
|
|
311
336
|
}
|
|
312
|
-
|
|
337
|
+
const sheetTree = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
313
338
|
style: _reactNative.StyleSheet.absoluteFill,
|
|
314
339
|
pointerEvents: "box-none",
|
|
315
340
|
testID: testID,
|
|
316
341
|
accessibilityViewIsModal: accessibilityViewIsModal ?? isExpanded,
|
|
317
342
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
318
|
-
style: [styles.backdrop,
|
|
319
|
-
|
|
343
|
+
style: [styles.backdrop,
|
|
344
|
+
// Solid scrim — the theme `background.overlay` token bakes in alpha
|
|
345
|
+
// and double-multiplies with `backdropOpacity`, leaving the backdrop
|
|
346
|
+
// looking washed out. Standard modal scrims are solid black so the
|
|
347
|
+
// prop controls the actual final opacity.
|
|
348
|
+
{
|
|
349
|
+
backgroundColor: '#000'
|
|
320
350
|
}, backdropStyle],
|
|
321
351
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
322
352
|
style: _reactNative.StyleSheet.absoluteFill,
|
|
@@ -356,6 +386,22 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
|
|
|
356
386
|
})
|
|
357
387
|
})]
|
|
358
388
|
});
|
|
389
|
+
if (mode === 'modal') {
|
|
390
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
|
|
391
|
+
transparent: true,
|
|
392
|
+
visible: modalVisible,
|
|
393
|
+
onRequestClose: close,
|
|
394
|
+
statusBarTranslucent: true,
|
|
395
|
+
presentationStyle: "overFullScreen",
|
|
396
|
+
animationType: "none",
|
|
397
|
+
supportedOrientations: ['portrait', 'landscape'],
|
|
398
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureHandlerRootView, {
|
|
399
|
+
style: styles.modalRoot,
|
|
400
|
+
children: sheetTree
|
|
401
|
+
})
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return sheetTree;
|
|
359
405
|
});
|
|
360
406
|
BottomSheet.displayName = 'BottomSheet';
|
|
361
407
|
|
|
@@ -395,6 +441,9 @@ const buildStyles = _theme => _reactNative.StyleSheet.create({
|
|
|
395
441
|
},
|
|
396
442
|
content: {
|
|
397
443
|
flex: 1
|
|
444
|
+
},
|
|
445
|
+
modalRoot: {
|
|
446
|
+
flex: 1
|
|
398
447
|
}
|
|
399
448
|
});
|
|
400
449
|
var _default = exports.default = BottomSheet;
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
23
|
-
import { Dimensions, Keyboard, Platform, Pressable, StyleSheet, View } from 'react-native';
|
|
24
|
-
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
23
|
+
import { Dimensions, Keyboard, Modal, Platform, Pressable, StyleSheet, View } from 'react-native';
|
|
24
|
+
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
25
25
|
import Animated, { Extrapolation, interpolate, runOnJS, useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
|
|
26
26
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
27
27
|
import { useTheme } from "../../theme/index.js";
|
|
@@ -44,6 +44,7 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
44
44
|
enableBackdropPress = true,
|
|
45
45
|
backdropOpacity = 0.5,
|
|
46
46
|
keyboardBehavior = 'none',
|
|
47
|
+
mode = 'modal',
|
|
47
48
|
handleIndicatorStyle,
|
|
48
49
|
containerStyle,
|
|
49
50
|
children,
|
|
@@ -79,6 +80,10 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
79
80
|
const currentIndexShared = useSharedValue(-1);
|
|
80
81
|
const [currentIndex, setCurrentIndex] = useState(-1);
|
|
81
82
|
const isAnimatingRef = useRef(false);
|
|
83
|
+
// Drives the native <Modal>'s visible prop in modal mode. We mount the modal
|
|
84
|
+
// synchronously on open and unmount only after the close animation finishes
|
|
85
|
+
// so the slide-down stays visible.
|
|
86
|
+
const [modalVisible, setModalVisible] = useState(false);
|
|
82
87
|
|
|
83
88
|
// Convert a snap-point index → translateY position. -1 = closed.
|
|
84
89
|
const yForIndex = useCallback(idx => {
|
|
@@ -106,14 +111,26 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
106
111
|
}, [onAnimate, translateY]);
|
|
107
112
|
const markAnimationDone = useCallback(() => {
|
|
108
113
|
isAnimatingRef.current = false;
|
|
109
|
-
|
|
114
|
+
// If we just finished a close animation, unmount the modal wrapper.
|
|
115
|
+
if (mode === 'modal' && currentIndexShared.value < 0) {
|
|
116
|
+
setModalVisible(false);
|
|
117
|
+
}
|
|
118
|
+
}, [mode, currentIndexShared]);
|
|
110
119
|
const expand = useCallback(idx => {
|
|
111
120
|
const target = typeof idx === 'number' ? clamp(idx, 0, resolvedSnapPoints.length - 1) : currentIndex >= 0 ? currentIndex : 0;
|
|
112
121
|
const fromIndex = currentIndexShared.value;
|
|
113
122
|
const to = yForIndex(target);
|
|
123
|
+
// Mount the Modal before kicking off the spring so the sheet has a
|
|
124
|
+
// host to animate into.
|
|
125
|
+
if (mode === 'modal') setModalVisible(true);
|
|
126
|
+
// Reset translateY to the closed position so the first open animates
|
|
127
|
+
// up from off-screen rather than snapping in place.
|
|
128
|
+
if (fromIndex < 0) {
|
|
129
|
+
translateY.value = closedY;
|
|
130
|
+
}
|
|
114
131
|
setIndexJS(target);
|
|
115
132
|
animateTo(to, fromIndex, target);
|
|
116
|
-
}, [animateTo, currentIndex, currentIndexShared, resolvedSnapPoints.length, setIndexJS, yForIndex]);
|
|
133
|
+
}, [animateTo, closedY, currentIndex, currentIndexShared, mode, resolvedSnapPoints.length, setIndexJS, translateY, yForIndex]);
|
|
117
134
|
const collapse = useCallback(() => {
|
|
118
135
|
if (resolvedSnapPoints.length === 0) return;
|
|
119
136
|
const fromIndex = currentIndexShared.value;
|
|
@@ -265,10 +282,15 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
265
282
|
}, [keyboardBehavior, keyboardOffset]);
|
|
266
283
|
|
|
267
284
|
// ───────── Animated styles ─────────
|
|
285
|
+
const safeAreaTop = insets.top;
|
|
268
286
|
const sheetStyle = useAnimatedStyle(() => {
|
|
269
|
-
// Don't push past the screen top — keyboard offset is clamped to minTopY.
|
|
270
287
|
const yWithKb = translateY.value + keyboardOffset.value;
|
|
271
|
-
|
|
288
|
+
// When the keyboard is up, allow the sheet to slide above its natural
|
|
289
|
+
// max-snap-point top (`minTopY`) so the input area stays visible. We still
|
|
290
|
+
// clamp to the safe-area top so the sheet doesn't disappear under the
|
|
291
|
+
// status bar / notch.
|
|
292
|
+
const lowerBound = keyboardOffset.value < 0 ? safeAreaTop : minTopY;
|
|
293
|
+
const clamped = yWithKb < lowerBound ? lowerBound : yWithKb;
|
|
272
294
|
return {
|
|
273
295
|
transform: [{
|
|
274
296
|
translateY: clamped
|
|
@@ -295,23 +317,31 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
295
317
|
}, [enableBackdropPress, close]);
|
|
296
318
|
|
|
297
319
|
// Don't render the heavy gesture tree at all when nothing's been opened yet.
|
|
298
|
-
//
|
|
299
|
-
//
|
|
320
|
+
// For inline mode we keep the legacy "mount on first open" behavior; modal
|
|
321
|
+
// mode is gated entirely by `modalVisible`.
|
|
300
322
|
const everOpenedRef = useRef(false);
|
|
301
323
|
if (isExpanded || controlledIndex !== undefined) {
|
|
302
324
|
everOpenedRef.current = true;
|
|
303
325
|
}
|
|
304
|
-
if (!everOpenedRef.current) {
|
|
326
|
+
if (mode === 'inline' && !everOpenedRef.current) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
if (mode === 'modal' && !modalVisible) {
|
|
305
330
|
return null;
|
|
306
331
|
}
|
|
307
|
-
|
|
332
|
+
const sheetTree = /*#__PURE__*/_jsxs(View, {
|
|
308
333
|
style: StyleSheet.absoluteFill,
|
|
309
334
|
pointerEvents: "box-none",
|
|
310
335
|
testID: testID,
|
|
311
336
|
accessibilityViewIsModal: accessibilityViewIsModal ?? isExpanded,
|
|
312
337
|
children: [/*#__PURE__*/_jsx(Animated.View, {
|
|
313
|
-
style: [styles.backdrop,
|
|
314
|
-
|
|
338
|
+
style: [styles.backdrop,
|
|
339
|
+
// Solid scrim — the theme `background.overlay` token bakes in alpha
|
|
340
|
+
// and double-multiplies with `backdropOpacity`, leaving the backdrop
|
|
341
|
+
// looking washed out. Standard modal scrims are solid black so the
|
|
342
|
+
// prop controls the actual final opacity.
|
|
343
|
+
{
|
|
344
|
+
backgroundColor: '#000'
|
|
315
345
|
}, backdropStyle],
|
|
316
346
|
children: /*#__PURE__*/_jsx(Pressable, {
|
|
317
347
|
style: StyleSheet.absoluteFill,
|
|
@@ -351,6 +381,22 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
351
381
|
})
|
|
352
382
|
})]
|
|
353
383
|
});
|
|
384
|
+
if (mode === 'modal') {
|
|
385
|
+
return /*#__PURE__*/_jsx(Modal, {
|
|
386
|
+
transparent: true,
|
|
387
|
+
visible: modalVisible,
|
|
388
|
+
onRequestClose: close,
|
|
389
|
+
statusBarTranslucent: true,
|
|
390
|
+
presentationStyle: "overFullScreen",
|
|
391
|
+
animationType: "none",
|
|
392
|
+
supportedOrientations: ['portrait', 'landscape'],
|
|
393
|
+
children: /*#__PURE__*/_jsx(GestureHandlerRootView, {
|
|
394
|
+
style: styles.modalRoot,
|
|
395
|
+
children: sheetTree
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return sheetTree;
|
|
354
400
|
});
|
|
355
401
|
BottomSheet.displayName = 'BottomSheet';
|
|
356
402
|
|
|
@@ -390,6 +436,9 @@ const buildStyles = _theme => StyleSheet.create({
|
|
|
390
436
|
},
|
|
391
437
|
content: {
|
|
392
438
|
flex: 1
|
|
439
|
+
},
|
|
440
|
+
modalRoot: {
|
|
441
|
+
flex: 1
|
|
393
442
|
}
|
|
394
443
|
});
|
|
395
444
|
export { BottomSheet };
|
|
@@ -20,6 +20,7 @@ import React from 'react';
|
|
|
20
20
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
21
21
|
export type SnapPoint = number | `${number}%`;
|
|
22
22
|
export type KeyboardBehavior = 'none' | 'shift';
|
|
23
|
+
export type BottomSheetMode = 'modal' | 'inline';
|
|
23
24
|
export interface BottomSheetProps {
|
|
24
25
|
snapPoints: SnapPoint[];
|
|
25
26
|
index?: number;
|
|
@@ -37,6 +38,15 @@ export interface BottomSheetProps {
|
|
|
37
38
|
* sheet itself.
|
|
38
39
|
*/
|
|
39
40
|
keyboardBehavior?: KeyboardBehavior;
|
|
41
|
+
/**
|
|
42
|
+
* How the sheet is mounted in the view tree.
|
|
43
|
+
* - `'modal'` (default): rendered inside a native `<Modal>` so the backdrop
|
|
44
|
+
* covers the entire screen (status bar, headers, tab bars) — the standard
|
|
45
|
+
* bottom-sheet UX.
|
|
46
|
+
* - `'inline'`: rendered as an `absoluteFill` overlay inside the parent
|
|
47
|
+
* component. Useful when embedding a sheet within a bounded area.
|
|
48
|
+
*/
|
|
49
|
+
mode?: BottomSheetMode;
|
|
40
50
|
handleIndicatorStyle?: StyleProp<ViewStyle>;
|
|
41
51
|
containerStyle?: StyleProp<ViewStyle>;
|
|
42
52
|
children?: React.ReactNode;
|
|
@@ -20,6 +20,7 @@ import React from 'react';
|
|
|
20
20
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
21
21
|
export type SnapPoint = number | `${number}%`;
|
|
22
22
|
export type KeyboardBehavior = 'none' | 'shift';
|
|
23
|
+
export type BottomSheetMode = 'modal' | 'inline';
|
|
23
24
|
export interface BottomSheetProps {
|
|
24
25
|
snapPoints: SnapPoint[];
|
|
25
26
|
index?: number;
|
|
@@ -37,6 +38,15 @@ export interface BottomSheetProps {
|
|
|
37
38
|
* sheet itself.
|
|
38
39
|
*/
|
|
39
40
|
keyboardBehavior?: KeyboardBehavior;
|
|
41
|
+
/**
|
|
42
|
+
* How the sheet is mounted in the view tree.
|
|
43
|
+
* - `'modal'` (default): rendered inside a native `<Modal>` so the backdrop
|
|
44
|
+
* covers the entire screen (status bar, headers, tab bars) — the standard
|
|
45
|
+
* bottom-sheet UX.
|
|
46
|
+
* - `'inline'`: rendered as an `absoluteFill` overlay inside the parent
|
|
47
|
+
* component. Useful when embedding a sheet within a bounded area.
|
|
48
|
+
*/
|
|
49
|
+
mode?: BottomSheetMode;
|
|
40
50
|
handleIndicatorStyle?: StyleProp<ViewStyle>;
|
|
41
51
|
containerStyle?: StyleProp<ViewStyle>;
|
|
42
52
|
children?: React.ReactNode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority-technologies/mobile",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "Beautiful, animated, accessible React Native components plus API/auth/logging/network/storage utilities for Webority projects.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|