@oxyhq/bloom 0.12.1 → 0.14.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/lib/commonjs/fab/Fab.js +25 -6
- package/lib/commonjs/fab/Fab.js.map +1 -1
- package/lib/commonjs/fab/Fab.web.js +26 -7
- package/lib/commonjs/fab/Fab.web.js.map +1 -1
- package/lib/commonjs/index.js +15 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +21 -6
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.js +300 -0
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.js.map +1 -0
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.native.js +226 -0
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.native.js.map +1 -0
- package/lib/commonjs/responsive-sheet/const.js +60 -0
- package/lib/commonjs/responsive-sheet/const.js.map +1 -0
- package/lib/commonjs/responsive-sheet/index.js +19 -0
- package/lib/commonjs/responsive-sheet/index.js.map +1 -0
- package/lib/commonjs/responsive-sheet/index.web.js +19 -0
- package/lib/commonjs/responsive-sheet/index.web.js.map +1 -0
- package/lib/commonjs/responsive-sheet/types.js +6 -0
- package/lib/commonjs/responsive-sheet/types.js.map +1 -0
- package/lib/module/fab/Fab.js +25 -6
- package/lib/module/fab/Fab.js.map +1 -1
- package/lib/module/fab/Fab.web.js +26 -7
- package/lib/module/fab/Fab.web.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/responsive-sheet/ResponsiveSheet.js +293 -0
- package/lib/module/responsive-sheet/ResponsiveSheet.js.map +1 -0
- package/lib/module/responsive-sheet/ResponsiveSheet.native.js +219 -0
- package/lib/module/responsive-sheet/ResponsiveSheet.native.js.map +1 -0
- package/lib/module/responsive-sheet/const.js +56 -0
- package/lib/module/responsive-sheet/const.js.map +1 -0
- package/lib/module/responsive-sheet/index.js +4 -0
- package/lib/module/responsive-sheet/index.js.map +1 -0
- package/lib/module/responsive-sheet/index.web.js +14 -0
- package/lib/module/responsive-sheet/index.web.js.map +1 -0
- package/lib/module/responsive-sheet/types.js +4 -0
- package/lib/module/responsive-sheet/types.js.map +1 -0
- package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/types.d.ts +12 -2
- package/lib/typescript/commonjs/fab/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts +26 -0
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.native.d.ts +20 -0
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.native.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/const.d.ts +43 -0
- package/lib/typescript/commonjs/responsive-sheet/const.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/index.d.ts +3 -0
- package/lib/typescript/commonjs/responsive-sheet/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts +3 -0
- package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/types.d.ts +55 -0
- package/lib/typescript/commonjs/responsive-sheet/types.d.ts.map +1 -0
- package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/module/fab/types.d.ts +12 -2
- package/lib/typescript/module/fab/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts +26 -0
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.native.d.ts +20 -0
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.native.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/const.d.ts +43 -0
- package/lib/typescript/module/responsive-sheet/const.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/index.d.ts +3 -0
- package/lib/typescript/module/responsive-sheet/index.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/index.web.d.ts +3 -0
- package/lib/typescript/module/responsive-sheet/index.web.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/types.d.ts +55 -0
- package/lib/typescript/module/responsive-sheet/types.d.ts.map +1 -0
- package/package.json +18 -1
- package/src/__tests__/Fab.test.tsx +17 -0
- package/src/__tests__/Fab.web.test.tsx +14 -0
- package/src/__tests__/ResponsiveSheet.test.tsx +104 -0
- package/src/fab/Fab.stories.tsx +9 -0
- package/src/fab/Fab.tsx +33 -7
- package/src/fab/Fab.web.tsx +33 -8
- package/src/fab/types.ts +12 -2
- package/src/index.ts +8 -0
- package/src/index.web.ts +8 -0
- package/src/responsive-sheet/ResponsiveSheet.native.tsx +257 -0
- package/src/responsive-sheet/ResponsiveSheet.stories.tsx +54 -0
- package/src/responsive-sheet/ResponsiveSheet.tsx +327 -0
- package/src/responsive-sheet/const.ts +53 -0
- package/src/responsive-sheet/index.ts +6 -0
- package/src/responsive-sheet/index.web.ts +16 -0
- package/src/responsive-sheet/types.ts +57 -0
package/src/fab/Fab.web.tsx
CHANGED
|
@@ -14,15 +14,40 @@ import type { FabPlacement, FabProps, FabSize, FabVariant } from './types';
|
|
|
14
14
|
|
|
15
15
|
export type { FabProps, FabVariant, FabSize, FabPlacement } from './types';
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
interface ResolvedSize {
|
|
18
|
+
diameter: number;
|
|
19
|
+
iconBox: number;
|
|
20
|
+
fontSize: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Material-style FAB scale. `small` (40px) keeps a full 24px icon box so the
|
|
24
|
+
// glyph reads clearly; `medium` (56px) is the canonical FAB; `large` (64px) is
|
|
25
|
+
// the high-prominence action.
|
|
26
|
+
const SIZE_CONFIG: Record<FabSize, ResolvedSize> = {
|
|
21
27
|
small: { diameter: 40, iconBox: 24, fontSize: 14 },
|
|
22
28
|
medium: { diameter: 56, iconBox: 24, fontSize: 15 },
|
|
23
29
|
large: { diameter: 64, iconBox: 28, fontSize: 16 },
|
|
24
30
|
};
|
|
25
31
|
|
|
32
|
+
const MIN_NUMERIC_ICON_BOX = 22;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a `size` prop (preset name or raw pixel diameter) to concrete pixel
|
|
36
|
+
* geometry. Mirrors the native `resolveSize` exactly so a numeric size renders
|
|
37
|
+
* identically on both platforms. A numeric size sets the diameter directly and
|
|
38
|
+
* derives the icon box as `round(size * 0.5)` (clamped to a sensible minimum).
|
|
39
|
+
*/
|
|
40
|
+
function resolveSize(size: FabSize | number): ResolvedSize {
|
|
41
|
+
if (typeof size === 'number') {
|
|
42
|
+
return {
|
|
43
|
+
diameter: size,
|
|
44
|
+
iconBox: Math.max(MIN_NUMERIC_ICON_BOX, Math.round(size * 0.5)),
|
|
45
|
+
fontSize: Math.max(13, Math.round(size * 0.27)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return SIZE_CONFIG[size];
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
const PILL_RADIUS = 999;
|
|
27
52
|
const PRESS_SCALE = 0.94;
|
|
28
53
|
const DEFAULT_OFFSET = 16;
|
|
@@ -176,7 +201,7 @@ const FabWebComponent: React.FC<FabProps> = ({
|
|
|
176
201
|
const reactId = useId();
|
|
177
202
|
const resolvedId = id ?? `bloom-fab-${reactId}`;
|
|
178
203
|
|
|
179
|
-
const sizeConfig =
|
|
204
|
+
const sizeConfig = useMemo(() => resolveSize(size), [size]);
|
|
180
205
|
const isExtended = label != null && label.length > 0;
|
|
181
206
|
const content = icon ?? children;
|
|
182
207
|
const variantColors = useMemo(() => resolveVariant(variant, theme.colors), [variant, theme.colors]);
|
|
@@ -202,8 +227,9 @@ const FabWebComponent: React.FC<FabProps> = ({
|
|
|
202
227
|
...placementStyle(placement, offset),
|
|
203
228
|
};
|
|
204
229
|
if (isExtended) {
|
|
205
|
-
|
|
206
|
-
base.
|
|
230
|
+
const pad = sizeConfig.diameter <= 44 ? 14 : 20;
|
|
231
|
+
base.paddingLeft = pad;
|
|
232
|
+
base.paddingRight = pad;
|
|
207
233
|
} else {
|
|
208
234
|
base.width = sizeConfig.diameter;
|
|
209
235
|
}
|
|
@@ -218,7 +244,6 @@ const FabWebComponent: React.FC<FabProps> = ({
|
|
|
218
244
|
placement,
|
|
219
245
|
offset,
|
|
220
246
|
isExtended,
|
|
221
|
-
size,
|
|
222
247
|
]);
|
|
223
248
|
|
|
224
249
|
const handleClick = useCallback(
|
package/src/fab/types.ts
CHANGED
|
@@ -12,13 +12,19 @@ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
|
12
12
|
export type FabVariant = 'primary' | 'secondary' | 'surface';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* FAB
|
|
15
|
+
* FAB size presets, matching Material-ish conventions but in Bloom's scale:
|
|
16
16
|
* - `small` — 40px (compact / dense layouts)
|
|
17
17
|
* - `medium` — 56px (the canonical FAB; the default)
|
|
18
18
|
* - `large` — 64px (high-prominence primary action)
|
|
19
19
|
*
|
|
20
20
|
* In the `extended` variant the height tracks the size but the FAB grows
|
|
21
21
|
* horizontally to fit its icon + label as a pill.
|
|
22
|
+
*
|
|
23
|
+
* The `size` prop ALSO accepts a raw pixel `number` for an exact diameter
|
|
24
|
+
* between (or outside) the presets — e.g. `size={48}` for a FAB halfway between
|
|
25
|
+
* `small` and `medium`. A numeric size sets the diameter directly, derives the
|
|
26
|
+
* icon box as `round(size * 0.5)` (min 22px) and keeps the circle radius at
|
|
27
|
+
* `size / 2`.
|
|
22
28
|
*/
|
|
23
29
|
export type FabSize = 'small' | 'medium' | 'large';
|
|
24
30
|
|
|
@@ -73,7 +79,11 @@ export interface FabProps {
|
|
|
73
79
|
label?: string;
|
|
74
80
|
|
|
75
81
|
variant?: FabVariant;
|
|
76
|
-
|
|
82
|
+
/**
|
|
83
|
+
* A preset (`'small' | 'medium' | 'large'`) or a raw pixel diameter
|
|
84
|
+
* (`number`). Defaults to `'medium'`. See {@link FabSize}.
|
|
85
|
+
*/
|
|
86
|
+
size?: FabSize | number;
|
|
77
87
|
|
|
78
88
|
/**
|
|
79
89
|
* Corner of the containing block to anchor to. Defaults to `'bottom-right'`.
|
package/src/index.ts
CHANGED
|
@@ -88,6 +88,14 @@ export type { ComboboxProps, ComboboxOption } from './combobox';
|
|
|
88
88
|
export { BottomSheet } from './bottom-sheet';
|
|
89
89
|
export type { BottomSheetRef, BottomSheetProps } from './bottom-sheet';
|
|
90
90
|
|
|
91
|
+
// Responsive sheet — side-sheet on wide screens, bottom-sheet on narrow.
|
|
92
|
+
export { ResponsiveSheet, RESPONSIVE_SHEET_BACKDROP_TESTID } from './responsive-sheet';
|
|
93
|
+
export type {
|
|
94
|
+
ResponsiveSheetProps,
|
|
95
|
+
ResponsiveSheetSide,
|
|
96
|
+
ResponsiveSheetInset,
|
|
97
|
+
} from './responsive-sheet';
|
|
98
|
+
|
|
91
99
|
// Data display
|
|
92
100
|
export * from './card';
|
|
93
101
|
export * from './badge';
|
package/src/index.web.ts
CHANGED
|
@@ -93,6 +93,14 @@ export type { ComboboxProps, ComboboxOption } from './combobox/index.web';
|
|
|
93
93
|
export { BottomSheet } from './bottom-sheet';
|
|
94
94
|
export type { BottomSheetRef, BottomSheetProps } from './bottom-sheet';
|
|
95
95
|
|
|
96
|
+
// Responsive sheet — side-sheet on wide screens, bottom-sheet on narrow.
|
|
97
|
+
export { ResponsiveSheet, RESPONSIVE_SHEET_BACKDROP_TESTID } from './responsive-sheet/index.web';
|
|
98
|
+
export type {
|
|
99
|
+
ResponsiveSheetProps,
|
|
100
|
+
ResponsiveSheetSide,
|
|
101
|
+
ResponsiveSheetInset,
|
|
102
|
+
} from './responsive-sheet/index.web';
|
|
103
|
+
|
|
96
104
|
// Data display
|
|
97
105
|
export * from './card';
|
|
98
106
|
export * from './badge';
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import {
|
|
9
|
+
Pressable,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
useWindowDimensions,
|
|
12
|
+
View,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import Animated, {
|
|
16
|
+
cancelAnimation,
|
|
17
|
+
Easing,
|
|
18
|
+
runOnJS,
|
|
19
|
+
useAnimatedStyle,
|
|
20
|
+
useSharedValue,
|
|
21
|
+
withTiming,
|
|
22
|
+
} from 'react-native-reanimated';
|
|
23
|
+
|
|
24
|
+
import { Z_INDEX } from '../styles/z-index';
|
|
25
|
+
import { useTheme } from '../theme/use-theme';
|
|
26
|
+
import {
|
|
27
|
+
ANIMATION_DURATION,
|
|
28
|
+
BACKDROP_OPACITY,
|
|
29
|
+
DEFAULT_BREAKPOINT,
|
|
30
|
+
DEFAULT_LABEL,
|
|
31
|
+
DEFAULT_MAX_HEIGHT_RATIO,
|
|
32
|
+
DEFAULT_SIDE,
|
|
33
|
+
DEFAULT_WIDTH,
|
|
34
|
+
HANDLE_HEIGHT,
|
|
35
|
+
HANDLE_RADIUS,
|
|
36
|
+
HANDLE_WIDTH,
|
|
37
|
+
PANEL_RADIUS,
|
|
38
|
+
SIDE_SHEET_MIN_GUTTER,
|
|
39
|
+
} from './const';
|
|
40
|
+
import type { ResponsiveSheetProps } from './types';
|
|
41
|
+
|
|
42
|
+
/** Stable testID for the dimmed backdrop dismiss button. */
|
|
43
|
+
export const RESPONSIVE_SHEET_BACKDROP_TESTID = 'bloom-responsive-sheet-backdrop';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Native variant of `<ResponsiveSheet>` (selected only by Metro).
|
|
47
|
+
*
|
|
48
|
+
* Uses `react-native-reanimated` to drive the same motion as the web/CSS
|
|
49
|
+
* default fork: a shared `progress` value times 0 -> 1 on open and 1 -> 0 on
|
|
50
|
+
* close, mapped to the panel translate/opacity and the backdrop opacity.
|
|
51
|
+
* Native exit animations are safe here — the web-only `removeChild` crash does
|
|
52
|
+
* not apply — so we keep the node mounted through the close timing and unmount
|
|
53
|
+
* from the animation completion callback.
|
|
54
|
+
*
|
|
55
|
+
* The panel itself is measured (not percentage-translated): we offset by the
|
|
56
|
+
* panel's own width/height so the hidden state sits fully off its side, which
|
|
57
|
+
* is the reanimated-friendly equivalent of the web `translate(±100%)`.
|
|
58
|
+
*/
|
|
59
|
+
export function ResponsiveSheet({
|
|
60
|
+
open,
|
|
61
|
+
onClose,
|
|
62
|
+
side = DEFAULT_SIDE,
|
|
63
|
+
breakpoint = DEFAULT_BREAKPOINT,
|
|
64
|
+
width = DEFAULT_WIDTH,
|
|
65
|
+
maxHeightRatio = DEFAULT_MAX_HEIGHT_RATIO,
|
|
66
|
+
inset,
|
|
67
|
+
showHandle = true,
|
|
68
|
+
dismissOnBackdrop = true,
|
|
69
|
+
panelStyle,
|
|
70
|
+
panelClassName,
|
|
71
|
+
containerStyle,
|
|
72
|
+
containerClassName,
|
|
73
|
+
label = DEFAULT_LABEL,
|
|
74
|
+
children,
|
|
75
|
+
}: ResponsiveSheetProps) {
|
|
76
|
+
const theme = useTheme();
|
|
77
|
+
const { width: viewportWidth, height: viewportHeight } = useWindowDimensions();
|
|
78
|
+
|
|
79
|
+
// Keep the tree mounted through the exit timing; unmount once it settles.
|
|
80
|
+
const [mounted, setMounted] = useState(open);
|
|
81
|
+
// `progress`: 0 = fully hidden, 1 = fully shown. Drives both transform and
|
|
82
|
+
// opacity so a single timing controls the whole transition.
|
|
83
|
+
const progress = useSharedValue(open ? 1 : 0);
|
|
84
|
+
|
|
85
|
+
const isBottom = viewportWidth < breakpoint;
|
|
86
|
+
|
|
87
|
+
// Measured panel extent along the travel axis. Falls back to the geometry
|
|
88
|
+
// target so the very first frame is already off-screen before layout lands.
|
|
89
|
+
const insetTop = inset?.top ?? 0;
|
|
90
|
+
const insetBottom = inset?.bottom ?? 0;
|
|
91
|
+
const insetLeft = inset?.left ?? 0;
|
|
92
|
+
const insetRight = inset?.right ?? 0;
|
|
93
|
+
const anchorInset = side === 'left' ? insetLeft : insetRight;
|
|
94
|
+
const oppositeInset = side === 'left' ? insetRight : insetLeft;
|
|
95
|
+
|
|
96
|
+
const sideWidth = useMemo(() => {
|
|
97
|
+
const available = viewportWidth - anchorInset - oppositeInset - SIDE_SHEET_MIN_GUTTER;
|
|
98
|
+
return Math.max(0, Math.min(width, available));
|
|
99
|
+
}, [viewportWidth, anchorInset, oppositeInset, width]);
|
|
100
|
+
|
|
101
|
+
const bottomMaxHeight = useMemo(
|
|
102
|
+
() => Math.round(viewportHeight * maxHeightRatio),
|
|
103
|
+
[viewportHeight, maxHeightRatio],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Travel distance for the hidden state: the panel's measured extent (height
|
|
107
|
+
// for the bottom-sheet, width for the side-sheet), with the geometry target
|
|
108
|
+
// as the pre-measurement fallback.
|
|
109
|
+
const travel = useSharedValue(isBottom ? bottomMaxHeight : sideWidth);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
travel.value = isBottom ? bottomMaxHeight : sideWidth;
|
|
112
|
+
}, [isBottom, bottomMaxHeight, sideWidth, travel]);
|
|
113
|
+
|
|
114
|
+
const finishClose = useCallback(() => {
|
|
115
|
+
setMounted(false);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
cancelAnimation(progress);
|
|
120
|
+
const timing = { duration: ANIMATION_DURATION, easing: Easing.out(Easing.cubic) };
|
|
121
|
+
if (open) {
|
|
122
|
+
setMounted(true);
|
|
123
|
+
progress.value = withTiming(1, timing);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!mounted) return;
|
|
127
|
+
progress.value = withTiming(0, timing, (finished) => {
|
|
128
|
+
if (finished) runOnJS(finishClose)();
|
|
129
|
+
});
|
|
130
|
+
}, [open, mounted, progress, finishClose]);
|
|
131
|
+
|
|
132
|
+
const handleBackdropPress = useCallback(() => {
|
|
133
|
+
if (dismissOnBackdrop) onClose();
|
|
134
|
+
}, [dismissOnBackdrop, onClose]);
|
|
135
|
+
|
|
136
|
+
const backdropAnimatedStyle = useAnimatedStyle(() => ({
|
|
137
|
+
opacity: progress.value * BACKDROP_OPACITY,
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
const hiddenSign = side === 'left' ? -1 : 1;
|
|
141
|
+
const panelAnimatedStyle = useAnimatedStyle(() => {
|
|
142
|
+
const hidden = 1 - progress.value;
|
|
143
|
+
if (isBottom) {
|
|
144
|
+
return {
|
|
145
|
+
opacity: progress.value,
|
|
146
|
+
transform: [{ translateY: hidden * travel.value }],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
opacity: progress.value,
|
|
151
|
+
transform: [{ translateX: hidden * travel.value * hiddenSign }],
|
|
152
|
+
};
|
|
153
|
+
}, [isBottom, hiddenSign]);
|
|
154
|
+
|
|
155
|
+
const measurePanel = useCallback(
|
|
156
|
+
(e: { nativeEvent: { layout: { width: number; height: number } } }) => {
|
|
157
|
+
const measured = isBottom ? e.nativeEvent.layout.height : e.nativeEvent.layout.width;
|
|
158
|
+
if (measured > 0) travel.value = measured;
|
|
159
|
+
},
|
|
160
|
+
[isBottom, travel],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const panelGeometry = useMemo<ViewStyle>(() => {
|
|
164
|
+
if (isBottom) {
|
|
165
|
+
return {
|
|
166
|
+
left: 0,
|
|
167
|
+
right: 0,
|
|
168
|
+
bottom: 0,
|
|
169
|
+
maxHeight: bottomMaxHeight,
|
|
170
|
+
borderTopLeftRadius: PANEL_RADIUS,
|
|
171
|
+
borderTopRightRadius: PANEL_RADIUS,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
top: insetTop,
|
|
176
|
+
bottom: insetBottom,
|
|
177
|
+
[side]: anchorInset,
|
|
178
|
+
width: sideWidth,
|
|
179
|
+
borderRadius: PANEL_RADIUS,
|
|
180
|
+
} as ViewStyle;
|
|
181
|
+
}, [isBottom, bottomMaxHeight, insetTop, insetBottom, side, anchorInset, sideWidth]);
|
|
182
|
+
|
|
183
|
+
if (!mounted) return null;
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<View
|
|
187
|
+
style={[styles.root, containerStyle]}
|
|
188
|
+
{...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
|
|
189
|
+
pointerEvents="box-none"
|
|
190
|
+
>
|
|
191
|
+
<Animated.View style={[styles.backdrop, backdropAnimatedStyle]} pointerEvents="auto">
|
|
192
|
+
<Pressable
|
|
193
|
+
testID={RESPONSIVE_SHEET_BACKDROP_TESTID}
|
|
194
|
+
accessibilityRole="button"
|
|
195
|
+
accessibilityLabel={`Dismiss ${label}`}
|
|
196
|
+
onPress={handleBackdropPress}
|
|
197
|
+
disabled={!dismissOnBackdrop}
|
|
198
|
+
style={StyleSheet.absoluteFill}
|
|
199
|
+
/>
|
|
200
|
+
</Animated.View>
|
|
201
|
+
|
|
202
|
+
<Animated.View
|
|
203
|
+
accessibilityViewIsModal
|
|
204
|
+
{...(panelClassName ? ({ className: panelClassName } as Record<string, string>) : {})}
|
|
205
|
+
onLayout={measurePanel}
|
|
206
|
+
style={[
|
|
207
|
+
styles.panel,
|
|
208
|
+
{ backgroundColor: theme.colors.background, shadowColor: theme.colors.shadow },
|
|
209
|
+
panelGeometry,
|
|
210
|
+
panelAnimatedStyle,
|
|
211
|
+
panelStyle,
|
|
212
|
+
]}
|
|
213
|
+
pointerEvents="auto"
|
|
214
|
+
>
|
|
215
|
+
{isBottom && showHandle ? (
|
|
216
|
+
<View style={styles.handleRow} pointerEvents="none">
|
|
217
|
+
<View style={[styles.handle, { backgroundColor: theme.colors.border }]} />
|
|
218
|
+
</View>
|
|
219
|
+
) : null}
|
|
220
|
+
{children}
|
|
221
|
+
</Animated.View>
|
|
222
|
+
</View>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const styles = StyleSheet.create({
|
|
227
|
+
root: {
|
|
228
|
+
...StyleSheet.absoluteFill,
|
|
229
|
+
zIndex: Z_INDEX.fullscreen,
|
|
230
|
+
},
|
|
231
|
+
backdrop: {
|
|
232
|
+
...StyleSheet.absoluteFill,
|
|
233
|
+
backgroundColor: '#000',
|
|
234
|
+
},
|
|
235
|
+
panel: {
|
|
236
|
+
position: 'absolute',
|
|
237
|
+
overflow: 'hidden',
|
|
238
|
+
shadowOpacity: 0.18,
|
|
239
|
+
shadowRadius: 24,
|
|
240
|
+
shadowOffset: { width: 0, height: 8 },
|
|
241
|
+
elevation: 12,
|
|
242
|
+
},
|
|
243
|
+
handleRow: {
|
|
244
|
+
width: '100%',
|
|
245
|
+
alignItems: 'center',
|
|
246
|
+
paddingTop: 8,
|
|
247
|
+
paddingBottom: 4,
|
|
248
|
+
},
|
|
249
|
+
handle: {
|
|
250
|
+
width: HANDLE_WIDTH,
|
|
251
|
+
height: HANDLE_HEIGHT,
|
|
252
|
+
borderRadius: HANDLE_RADIUS,
|
|
253
|
+
opacity: 0.5,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
export default ResponsiveSheet;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { ResponsiveSheet } from './index';
|
|
6
|
+
import type { ResponsiveSheetSide } from './types';
|
|
7
|
+
import { Button } from '../button';
|
|
8
|
+
import { Text } from '../typography';
|
|
9
|
+
|
|
10
|
+
const meta: Meta = {
|
|
11
|
+
title: 'Overlays/ResponsiveSheet',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
|
|
16
|
+
type Story = StoryObj;
|
|
17
|
+
|
|
18
|
+
function Demo({ side }: { side: ResponsiveSheetSide }) {
|
|
19
|
+
const [open, setOpen] = useState(false);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<View style={{ padding: 24, minHeight: 480 }}>
|
|
23
|
+
<Button onPress={() => setOpen(true)}>Open sheet ({side})</Button>
|
|
24
|
+
<ResponsiveSheet
|
|
25
|
+
open={open}
|
|
26
|
+
onClose={() => setOpen(false)}
|
|
27
|
+
side={side}
|
|
28
|
+
width={420}
|
|
29
|
+
inset={{ top: 16, bottom: 16, left: 16 }}
|
|
30
|
+
label="Demo sheet"
|
|
31
|
+
>
|
|
32
|
+
<View style={{ padding: 20, gap: 12 }}>
|
|
33
|
+
<Text style={{ fontSize: 20, fontWeight: '700' }}>Responsive sheet</Text>
|
|
34
|
+
<Text>
|
|
35
|
+
On a wide viewport this is an anchored side-sheet; below the
|
|
36
|
+
breakpoint it becomes a bottom-sheet with a drag handle. Resize the
|
|
37
|
+
preview to switch modes.
|
|
38
|
+
</Text>
|
|
39
|
+
<Button variant="secondary" onPress={() => setOpen(false)}>
|
|
40
|
+
Close
|
|
41
|
+
</Button>
|
|
42
|
+
</View>
|
|
43
|
+
</ResponsiveSheet>
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const SideLeft: Story = {
|
|
49
|
+
render: () => <Demo side="left" />,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const SideRight: Story = {
|
|
53
|
+
render: () => <Demo side="right" />,
|
|
54
|
+
};
|