@mrmeg/expo-ui 0.27.0 → 0.28.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/CHANGELOG.md +712 -0
- package/LLM_USAGE.md +260 -32
- package/README.md +558 -101
- package/dist/components/Accordion.js +12 -6
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +5 -2
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Badge.js +5 -1
- package/dist/components/BottomSheet.d.ts +66 -9
- package/dist/components/BottomSheet.js +143 -39
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +64 -43
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +24 -4
- package/dist/components/Carousel.js +2 -2
- package/dist/components/Checkbox.js +51 -44
- package/dist/components/Collapsible.js +7 -3
- package/dist/components/Dialog.js +178 -29
- package/dist/components/Drawer.d.ts +1 -1
- package/dist/components/Drawer.js +65 -55
- package/dist/components/DropdownMenu.js +11 -7
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +5 -2
- package/dist/components/Hydrated.d.ts +31 -0
- package/dist/components/Hydrated.js +30 -0
- package/dist/components/Icon.d.ts +17 -5
- package/dist/components/Icon.js +31 -10
- package/dist/components/InputOTP.js +7 -3
- package/dist/components/Item.d.ts +103 -4
- package/dist/components/Item.js +168 -15
- package/dist/components/KeyboardAvoidingView.d.ts +1 -1
- package/dist/components/KeyboardAvoidingView.js +4 -1
- package/dist/components/Label.js +3 -2
- package/dist/components/MaxWidthContainer.js +1 -1
- package/dist/components/Notification.js +45 -27
- package/dist/components/Popover.d.ts +18 -5
- package/dist/components/Popover.js +72 -8
- package/dist/components/Progress.js +8 -6
- package/dist/components/RadioGroup.js +52 -49
- package/dist/components/Screen.d.ts +61 -0
- package/dist/components/Screen.js +62 -0
- package/dist/components/SectionHeader.d.ts +1 -1
- package/dist/components/SegmentedControl.d.ts +12 -4
- package/dist/components/SegmentedControl.js +94 -4
- package/dist/components/Select.js +21 -26
- package/dist/components/Skeleton.js +24 -10
- package/dist/components/Slider.d.ts +11 -3
- package/dist/components/Slider.js +74 -6
- package/dist/components/StatCard.d.ts +4 -2
- package/dist/components/StatCard.js +3 -1
- package/dist/components/StyledText.d.ts +7 -1
- package/dist/components/StyledText.js +5 -3
- package/dist/components/Switch.d.ts +1 -1
- package/dist/components/Switch.js +23 -32
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +13 -24
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +73 -27
- package/dist/components/Toggle.d.ts +2 -42
- package/dist/components/Toggle.js +38 -27
- package/dist/components/ToggleGroup.d.ts +1 -1
- package/dist/components/ToggleGroup.js +37 -32
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +9 -3
- package/dist/components/UIProvider.d.ts +22 -2
- package/dist/components/UIProvider.js +15 -3
- package/dist/components/bottomSheetDismiss.d.ts +19 -0
- package/dist/components/bottomSheetDismiss.js +44 -0
- package/dist/components/bottomSheetHostSupport.d.ts +46 -0
- package/dist/components/bottomSheetHostSupport.js +67 -0
- package/dist/components/index.d.ts +46 -44
- package/dist/components/index.js +2 -0
- package/dist/components/keyboardDismiss.d.ts +25 -0
- package/dist/components/keyboardDismiss.js +60 -3
- package/dist/components/nativeTextField.android.js +13 -9
- package/dist/constants/colors.d.ts +40 -5
- package/dist/constants/colors.js +26 -8
- package/dist/constants/fonts.d.ts +41 -2
- package/dist/constants/fonts.js +46 -7
- package/dist/constants/index.d.ts +5 -4
- package/dist/constants/index.js +1 -0
- package/dist/constants/interaction.d.ts +18 -0
- package/dist/constants/interaction.js +18 -0
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/design-system.json +154 -2
- package/dist/hooks/index.d.ts +12 -7
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useDimensions.d.ts +13 -8
- package/dist/hooks/useDimensions.js +123 -48
- package/dist/hooks/useFocusVisible.d.ts +30 -0
- package/dist/hooks/useFocusVisible.js +45 -0
- package/dist/hooks/useFontStyle.d.ts +2 -2
- package/dist/hooks/useFontStyle.js +3 -2
- package/dist/hooks/useHydrated.d.ts +13 -0
- package/dist/hooks/useHydrated.js +19 -0
- package/dist/hooks/useNotificationOffset.d.ts +20 -0
- package/dist/hooks/useNotificationOffset.js +34 -0
- package/dist/hooks/useResources.d.ts +21 -20
- package/dist/hooks/useResources.js +72 -68
- package/dist/hooks/useScalePress.d.ts +8 -5
- package/dist/hooks/useScalePress.js +16 -9
- package/dist/hooks/useShape.d.ts +24 -0
- package/dist/hooks/useShape.js +24 -0
- package/dist/hooks/useStaggeredEntrance.js +9 -7
- package/dist/hooks/useTheme.d.ts +3 -3
- package/dist/hooks/useTheme.js +177 -66
- package/dist/hooks/useWindowInsets.d.ts +13 -0
- package/dist/hooks/useWindowInsets.js +23 -0
- package/dist/index.d.ts +5 -5
- package/dist/lib/composeRefs.d.ts +8 -0
- package/dist/lib/composeRefs.js +20 -0
- package/dist/lib/haptics.d.ts +12 -0
- package/dist/lib/haptics.js +21 -0
- package/dist/lib/index.d.ts +6 -4
- package/dist/lib/index.js +2 -0
- package/dist/lib/interFonts.d.ts +15 -0
- package/dist/lib/interFonts.js +1 -0
- package/dist/lib/interFonts.native.d.ts +3 -0
- package/dist/lib/interFonts.native.js +20 -0
- package/dist/lib/popoverPlacement.d.ts +36 -0
- package/dist/lib/popoverPlacement.js +29 -0
- package/dist/lib/stateSurface.d.ts +28 -0
- package/dist/lib/stateSurface.js +30 -0
- package/dist/lib/themedStyles.d.ts +1 -1
- package/dist/lib/useAnimatedValue.d.ts +15 -0
- package/dist/lib/useAnimatedValue.js +19 -0
- package/dist/lib/webHitTarget.d.ts +40 -0
- package/dist/lib/webHitTarget.js +37 -0
- package/dist/state/feedbackStore.d.ts +24 -0
- package/dist/state/feedbackStore.js +16 -0
- package/dist/state/globalUIStore.d.ts +27 -0
- package/dist/state/globalUIStore.js +23 -0
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +2 -0
- package/dist/state/initialScheme.d.ts +27 -0
- package/dist/state/initialScheme.js +29 -0
- package/dist/state/notify.d.ts +1 -1
- package/dist/state/themeColorScope.d.ts +1 -1
- package/dist/state/themeStore.d.ts +69 -16
- package/dist/state/themeStore.js +65 -38
- package/llms-full.md +56 -8
- package/llms.txt +10 -1
- package/package.json +69 -2
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect,
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
3
|
import { Animated, Platform, Pressable, View } from "react-native";
|
|
4
4
|
import { Icon } from "./Icon.js";
|
|
5
|
-
import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
|
|
5
|
+
import { TextClassContext, TextSelectabilityContext } from "./StyledText.context.js";
|
|
6
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
7
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
8
8
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
9
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
10
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
9
11
|
import { spacing } from "../constants/spacing.js";
|
|
12
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
10
13
|
import * as AccordionPrimitive from "@rn-primitives/accordion";
|
|
11
14
|
function normalizeSingleValue(value) {
|
|
12
15
|
return value ?? "";
|
|
@@ -98,21 +101,22 @@ const Trigger = Platform.OS === "web" ? View : Pressable;
|
|
|
98
101
|
* Includes animated chevron icon
|
|
99
102
|
*/
|
|
100
103
|
function AccordionTrigger({ children, style: styleOverride, disabled, ...props }) {
|
|
101
|
-
const { theme } = useTheme();
|
|
104
|
+
const { theme, getFocusRingStyle } = useTheme();
|
|
102
105
|
const reduceMotion = useReducedMotion();
|
|
103
106
|
const { isExpanded } = AccordionPrimitive.useItemContext();
|
|
104
|
-
const rotation =
|
|
107
|
+
const rotation = useAnimatedValue(isExpanded ? 1 : 0);
|
|
105
108
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
106
109
|
disabled: !!disabled,
|
|
107
110
|
scaleTo: 0.97,
|
|
108
111
|
haptic: false,
|
|
109
112
|
});
|
|
113
|
+
const focus = useFocusVisible();
|
|
110
114
|
useEffect(() => {
|
|
111
115
|
const target = isExpanded ? 1 : 0;
|
|
112
116
|
Animated.timing(rotation, {
|
|
113
117
|
toValue: target,
|
|
114
118
|
duration: reduceMotion ? 0 : isExpanded ? 200 : 150,
|
|
115
|
-
useNativeDriver:
|
|
119
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
116
120
|
}).start();
|
|
117
121
|
}, [isExpanded, reduceMotion, rotation]);
|
|
118
122
|
const chevronStyle = {
|
|
@@ -125,7 +129,7 @@ function AccordionTrigger({ children, style: styleOverride, disabled, ...props }
|
|
|
125
129
|
},
|
|
126
130
|
],
|
|
127
131
|
};
|
|
128
|
-
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(AccordionPrimitive.Header, { children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(AccordionPrimitive.Trigger, { ...props, disabled: disabled, asChild: true, children: _jsxs(Trigger, { onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, style: [
|
|
132
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(AccordionPrimitive.Header, { children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(AccordionPrimitive.Trigger, { ...props, disabled: disabled, asChild: true, children: _jsxs(Trigger, { onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: focus.onFocus, onBlur: focus.onBlur, style: [
|
|
129
133
|
{
|
|
130
134
|
flexDirection: "row",
|
|
131
135
|
alignItems: "center",
|
|
@@ -136,8 +140,10 @@ function AccordionTrigger({ children, style: styleOverride, disabled, ...props }
|
|
|
136
140
|
...(Platform.OS === "web" && {
|
|
137
141
|
cursor: "pointer",
|
|
138
142
|
userSelect: "none",
|
|
143
|
+
outlineStyle: "none",
|
|
139
144
|
}),
|
|
140
145
|
},
|
|
146
|
+
focus.focused && !disabled && getFocusRingStyle(),
|
|
141
147
|
// Spread array styles from primitives to prevent nested arrays on web
|
|
142
148
|
...(styleOverride && typeof styleOverride !== "function"
|
|
143
149
|
? (Array.isArray(styleOverride) ? styleOverride : [styleOverride])
|
|
@@ -51,5 +51,5 @@ interface AnimatedViewProps extends ViewProps {
|
|
|
51
51
|
* </AnimatedView>
|
|
52
52
|
* ```
|
|
53
53
|
*/
|
|
54
|
-
export declare function AnimatedView({ children, type, enterDuration, delay, style, ...props }: AnimatedViewProps): React.JSX.Element;
|
|
54
|
+
export declare function AnimatedView({ children, type, enterDuration, delay, style, pointerEvents, ...props }: AnimatedViewProps): React.JSX.Element;
|
|
55
55
|
export {};
|
|
@@ -29,11 +29,14 @@ import { useStaggeredEntrance } from "../hooks/useStaggeredEntrance.js";
|
|
|
29
29
|
* </AnimatedView>
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
|
-
export function AnimatedView({ children, type = "fade", enterDuration = 200, delay = 0, style, ...props }) {
|
|
32
|
+
export function AnimatedView({ children, type = "fade", enterDuration = 200, delay = 0, style, pointerEvents, ...props }) {
|
|
33
33
|
const entranceStyle = useStaggeredEntrance({
|
|
34
34
|
type,
|
|
35
35
|
delay,
|
|
36
36
|
duration: enterDuration,
|
|
37
37
|
});
|
|
38
|
-
|
|
38
|
+
// The `pointerEvents` prop is deprecated on web (react-native-web warns and
|
|
39
|
+
// lazily loads its warning module mid-render, which overflowed the first SSR
|
|
40
|
+
// render of deep routes); keep accepting it for consumers, but as a style.
|
|
41
|
+
return (_jsx(Animated.View, { style: [style, pointerEvents ? { pointerEvents } : null, entranceStyle], ...props, children: children }));
|
|
39
42
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type ImageSourcePropType, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
-
import { type IconName } from "./Icon";
|
|
3
|
+
import { type IconName } from "./Icon.js";
|
|
4
4
|
/** Named size tokens, or an explicit pixel diameter. */
|
|
5
5
|
export type AvatarSize = "sm" | "md" | "lg" | number;
|
|
6
6
|
export type AvatarShape = "circle" | "square";
|
|
@@ -223,7 +223,7 @@ const SIZE_TOKEN_FOR_PX = {
|
|
|
223
223
|
// Geometry carries no theme values, so a plain module-scope sheet is enough to
|
|
224
224
|
// get these rules into the prerendered <head>. Only colors go through
|
|
225
225
|
// createThemedStyles below. See "Enable Server Output" in docs/server-guide.md.
|
|
226
|
-
const geometry = StyleSheet.create({
|
|
226
|
+
const geometry = /*#__PURE__*/ StyleSheet.create({
|
|
227
227
|
base: {
|
|
228
228
|
alignItems: "center",
|
|
229
229
|
justifyContent: "center",
|
|
@@ -297,5 +297,5 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
297
297
|
color: theme.colors.accentForeground,
|
|
298
298
|
},
|
|
299
299
|
});
|
|
300
|
-
const themedStyles = createThemedStyles(createStyles);
|
|
300
|
+
const themedStyles = /*#__PURE__*/ createThemedStyles(createStyles);
|
|
301
301
|
export { Avatar, AvatarGroup };
|
package/dist/components/Badge.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { View, StyleSheet } from "react-native";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
|
+
import { shapeRadius, useShape } from "../hooks/useShape.js";
|
|
5
6
|
import { spacing } from "../constants/spacing.js";
|
|
6
7
|
import { StyledText } from "./StyledText.js";
|
|
7
8
|
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
@@ -21,6 +22,8 @@ import { createThemedStyles } from "../lib/themedStyles.js";
|
|
|
21
22
|
function Badge({ children, text, variant = "default", style: styleOverride }) {
|
|
22
23
|
const { theme } = useTheme();
|
|
23
24
|
const styles = themedStyles(theme);
|
|
25
|
+
// Host-app `setShape({ badge })`; the default is a pill.
|
|
26
|
+
const badgeRadius = shapeRadius(useShape("badge"));
|
|
24
27
|
const badgeContent = text ?? children;
|
|
25
28
|
const textStyle = [
|
|
26
29
|
styles.text,
|
|
@@ -43,6 +46,7 @@ function Badge({ children, text, variant = "default", style: styleOverride }) {
|
|
|
43
46
|
}));
|
|
44
47
|
return (_jsx(View, { accessibilityRole: "text", style: [
|
|
45
48
|
styles.badge,
|
|
49
|
+
badgeRadius,
|
|
46
50
|
variant === "default" && styles.default,
|
|
47
51
|
variant === "secondary" && styles.secondary,
|
|
48
52
|
variant === "outline" && styles.outline,
|
|
@@ -75,5 +79,5 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
75
79
|
userSelect: "none",
|
|
76
80
|
},
|
|
77
81
|
});
|
|
78
|
-
const themedStyles = createThemedStyles(createStyles);
|
|
82
|
+
const themedStyles = /*#__PURE__*/ createThemedStyles(createStyles);
|
|
79
83
|
export { Badge };
|
|
@@ -20,8 +20,18 @@ import { ViewProps, StyleProp, ViewStyle, ScrollViewProps } from "react-native";
|
|
|
20
20
|
* nothing in JS can measure the column in screen space (`measureInWindow`
|
|
21
21
|
* inside the `layoutRoot` sheet host reports host-relative coordinates), and a
|
|
22
22
|
* window-height estimate would lift a lifted sheet twice. Android: Material3's
|
|
23
|
-
* `ModalBottomSheet`
|
|
24
|
-
*
|
|
23
|
+
* `ModalBottomSheet` shrinks the sheet with `imePadding()`, `@expo/ui`'s
|
|
24
|
+
* `RNHostView` re-reports its Compose size to the shadow tree, and the
|
|
25
|
+
* `flexGrow: 1, height: 0` column the host wraps our children in follows, so
|
|
26
|
+
* `Footer` and the tail of `Body` stay above the keyboard. That report is
|
|
27
|
+
* delivered reliably only by `expo-modules-core` >= 57.0.4 (expo/expo#47778,
|
|
28
|
+
* fixed by #47810): older cores flushed it from a pre-draw listener on the
|
|
29
|
+
* activity window, which does not draw while the sheet's dialog window animates
|
|
30
|
+
* the IME, so the column kept its detent height until the activity redrew. The
|
|
31
|
+
* package warns once in dev on Android below that floor
|
|
32
|
+
* (`bottomSheetHostSupport.ts`). No JS keyboard signal exists inside the
|
|
33
|
+
* Compose dialog window, and none is needed. Web: none. Never nest a
|
|
34
|
+
* `KeyboardAvoidingView` in a sheet.
|
|
25
35
|
*
|
|
26
36
|
* Platform-owned behaviors (props accepted for ergonomics, but the platform
|
|
27
37
|
* decides):
|
|
@@ -37,7 +47,31 @@ import { ViewProps, StyleProp, ViewStyle, ScrollViewProps } from "react-native";
|
|
|
37
47
|
* - `Body` sets `keyboardShouldPersistTaps="always"` on its ScrollView so RN's
|
|
38
48
|
* own tap-dismissal never claims the first tap on a chip, button or field
|
|
39
49
|
* while a sheet field is focused; the `Content` boundary owns tap-away
|
|
40
|
-
* dismissal instead.
|
|
50
|
+
* dismissal instead. That covers ScrollViews *inside* the sheet only. The
|
|
51
|
+
* sheet's content is drawn in another native window but stays in the
|
|
52
|
+
* screen's React tree, and RN's responder negotiation walks that tree, so a
|
|
53
|
+
* ScrollView *around* the `BottomSheet` with the default
|
|
54
|
+
* `keyboardShouldPersistTaps="never"` claims a tap on any sheet control
|
|
55
|
+
* (`Footer` included) in the capture phase once a field is focused and
|
|
56
|
+
* blurs the field on release: the keyboard closes and the control never
|
|
57
|
+
* fires (Pixel_10 / API 36: 0 of 3 taps inside a default ScrollView, 3 of 3
|
|
58
|
+
* inside `always` or a plain View). Nothing inside the tree can preempt a
|
|
59
|
+
* capture-phase claim, so `Content` warns once in dev on Android when it
|
|
60
|
+
* observes one (`useAncestorClaimWarning`); the remedy is
|
|
61
|
+
* `keyboardShouldPersistTaps="always"` / `"handled"` on scroll views that
|
|
62
|
+
* contain a sheet, or `DismissKeyboard`, which already sets it.
|
|
63
|
+
*
|
|
64
|
+
* Dismiss completion: `onDismissed` (root prop) fires once per close after the
|
|
65
|
+
* sheet is fully gone, so the next modal (an RN `Modal`-backed `Dialog`, a
|
|
66
|
+
* native stack modal) can be presented from it without UIKit rejecting the
|
|
67
|
+
* presentation while the sheet is still animating out. iOS: `@expo/ui` raises
|
|
68
|
+
* its close callback from the native `onDismiss` event of SwiftUI
|
|
69
|
+
* `.sheet(isPresented:onDismiss:)`, which runs after the dismissal transition.
|
|
70
|
+
* Android: `@expo/ui` raises it after Material's hide animation for a swipe /
|
|
71
|
+
* back / scrim dismissal, and with the removal of the Compose sheet for a
|
|
72
|
+
* prop-driven close (which has no exit animation). Web: `@expo/ui` raises it
|
|
73
|
+
* before its exit animation, so the package waits for the HTML `<dialog>`'s
|
|
74
|
+
* `close` event instead (`bottomSheetDismiss.ts`).
|
|
41
75
|
*
|
|
42
76
|
* Scrollable bodies: the native sheet doesn't bound the hosted RN content to
|
|
43
77
|
* the detent height, so a tall `Body` overflows and clips its footer/tail. When
|
|
@@ -94,12 +128,28 @@ interface BottomSheetContextValue {
|
|
|
94
128
|
/** Whether a `Footer` is mounted, so `Body` can own the bottom safe-area inset when it isn't. */
|
|
95
129
|
hasFooter: boolean;
|
|
96
130
|
setHasFooter: (present: boolean) => void;
|
|
131
|
+
/** Called by `Content` once the native sheet has fully dismissed; invokes the root's `onDismissed`. */
|
|
132
|
+
notifyDismissed: () => void;
|
|
97
133
|
}
|
|
98
134
|
interface BottomSheetProps {
|
|
99
135
|
/** Controlled open state. Omit to use uncontrolled mode with `defaultOpen`. */
|
|
100
136
|
open?: boolean;
|
|
101
137
|
/** Callback when open state changes. */
|
|
102
138
|
onOpenChange?: (open: boolean) => void;
|
|
139
|
+
/**
|
|
140
|
+
* Called once per close, after the sheet has fully dismissed on every
|
|
141
|
+
* platform. Present the next modal (a `Dialog`, a native stack modal) from
|
|
142
|
+
* here rather than from `onOpenChange(false)` or a timer: on iOS the
|
|
143
|
+
* `UISheetPresentationController` is still dismissing when `onOpenChange`
|
|
144
|
+
* fires, and a `Modal` presented then is rejected ("already presenting")
|
|
145
|
+
* without retry. Detection — iOS: `@expo/ui`'s native `onDismiss` event
|
|
146
|
+
* (SwiftUI `.sheet(onDismiss:)`, after the transition). Android: `@expo/ui`'s
|
|
147
|
+
* close callback (after Material's hide animation for swipe / back / scrim; a
|
|
148
|
+
* prop-driven close removes the Compose sheet with no exit animation). Web:
|
|
149
|
+
* the HTML `<dialog>`'s `close` event, which `@expo/ui` raises when its exit
|
|
150
|
+
* animation ends.
|
|
151
|
+
*/
|
|
152
|
+
onDismissed?: () => void;
|
|
103
153
|
/** Initial open state for uncontrolled mode. Default: false. */
|
|
104
154
|
defaultOpen?: boolean;
|
|
105
155
|
/** Snap point heights (px or percentage strings). Default: ["50%"]. */
|
|
@@ -138,11 +188,12 @@ interface BottomSheetContentProps extends ViewProps {
|
|
|
138
188
|
/** Accepted for call-site ergonomics; ignored (platform owns keyboard). */
|
|
139
189
|
dismissKeyboardOnDrag?: boolean;
|
|
140
190
|
/**
|
|
141
|
-
* Style for the native sheet surface
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
191
|
+
* Style for the native sheet surface — the web (vaul) panel, Android
|
|
192
|
+
* `containerColor`, iOS `presentationBackground`. Merged over the themed card
|
|
193
|
+
* default. This is the sheet's only background: the RN content column paints
|
|
194
|
+
* no fill of its own, so a translucent color here reads as one layer from the
|
|
195
|
+
* grabber down, and `{ backgroundColor: "transparent" }` lets custom chrome
|
|
196
|
+
* (e.g. a glass backdrop) show through with nothing to clear via `style`.
|
|
146
197
|
*/
|
|
147
198
|
backgroundStyle?: StyleProp<ViewStyle>;
|
|
148
199
|
style?: StyleProp<ViewStyle>;
|
|
@@ -165,8 +216,14 @@ interface BottomSheetCloseProps {
|
|
|
165
216
|
children: React.ReactNode;
|
|
166
217
|
style?: StyleProp<ViewStyle>;
|
|
167
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Dev warning (Android) when an ancestor claimed a tap inside the sheet before
|
|
221
|
+
* the sheet was asked, while a package field held focus. See the header and
|
|
222
|
+
* `useAncestorClaimWarning`.
|
|
223
|
+
*/
|
|
224
|
+
export declare const bottomSheetAncestorClaimWarning: string;
|
|
168
225
|
declare function useBottomSheetContext(): BottomSheetContextValue;
|
|
169
|
-
declare function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen, snapPoints, closeOnBackdropPress, children, }: BottomSheetProps): React.JSX.Element;
|
|
226
|
+
declare function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, onDismissed, defaultOpen, snapPoints, closeOnBackdropPress, children, }: BottomSheetProps): React.JSX.Element;
|
|
170
227
|
declare function BottomSheetTrigger({ asChild, children, style: styleOverride }: BottomSheetTriggerProps): React.JSX.Element;
|
|
171
228
|
declare function BottomSheetContent({ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, testID, children, }: BottomSheetContentProps): React.JSX.Element;
|
|
172
229
|
declare function BottomSheetHandle({ style }: BottomSheetHandleProps): React.JSX.Element;
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext, use, useCallback, useEffect, useMemo, useReducer, useRef } from "react";
|
|
2
|
+
import React, { createContext, use, useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef } from "react";
|
|
3
3
|
import { View, Pressable, StyleSheet, ScrollView, Platform, Animated, useWindowDimensions, } from "react-native";
|
|
4
4
|
import { BottomSheet as NativeBottomSheet } from "@expo/ui/community/bottom-sheet";
|
|
5
|
-
import {
|
|
5
|
+
import { useWindowInsets } from "../hooks/useWindowInsets.js";
|
|
6
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
|
+
import { useShape } from "../hooks/useShape.js";
|
|
7
8
|
import { spacing } from "../constants/spacing.js";
|
|
8
9
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
9
|
-
import { TextColorContext, TextClassContext } from "./StyledText.context";
|
|
10
|
+
import { TextColorContext, TextClassContext } from "./StyledText.context.js";
|
|
10
11
|
import { Icon } from "./Icon.js";
|
|
11
|
-
import { useKeyboardDismissResponder } from "./keyboardDismiss.js";
|
|
12
|
+
import { useAncestorClaimWarning, useKeyboardDismissResponder } from "./keyboardDismiss.js";
|
|
13
|
+
import { warnIfBottomSheetHostDropsResize } from "./bottomSheetHostSupport.js";
|
|
14
|
+
import { awaitDialogClose } from "./bottomSheetDismiss.js";
|
|
12
15
|
const DEFAULT_SNAP_POINTS = ["50%"];
|
|
16
|
+
/**
|
|
17
|
+
* UIKit keeps a `.large` sheet's top edge 10 pt below the top safe-area inset;
|
|
18
|
+
* SwiftUI's `.fraction()` detents are fractions of the height that remains.
|
|
19
|
+
*/
|
|
20
|
+
const IOS_SHEET_TOP_GAP = 10;
|
|
21
|
+
/**
|
|
22
|
+
* `@expo/ui` pads the hosted RN column 16 pt below the native drag indicator
|
|
23
|
+
* (`paddingTop: 16` in its iOS `BottomSheet` when `handleComponent !== null`).
|
|
24
|
+
*/
|
|
25
|
+
const IOS_HOST_HANDLE_PADDING = 16;
|
|
13
26
|
function clampSnapIndex(index, maxSnapIndex) {
|
|
14
27
|
return Math.min(Math.max(index, 0), maxSnapIndex);
|
|
15
28
|
}
|
|
@@ -86,6 +99,18 @@ function bottomSheetRootReducer(state, action) {
|
|
|
86
99
|
// ============================================================================
|
|
87
100
|
// Context
|
|
88
101
|
// ============================================================================
|
|
102
|
+
/**
|
|
103
|
+
* Dev warning (Android) when an ancestor claimed a tap inside the sheet before
|
|
104
|
+
* the sheet was asked, while a package field held focus. See the header and
|
|
105
|
+
* `useAncestorClaimWarning`.
|
|
106
|
+
*/
|
|
107
|
+
export const bottomSheetAncestorClaimWarning = "@mrmeg/expo-ui BottomSheet: an ancestor claimed a tap inside the sheet before the sheet " +
|
|
108
|
+
"was asked, while a sheet field was focused, so the tapped control did not fire and the " +
|
|
109
|
+
"keyboard was dismissed instead. That is usually a ScrollView around the BottomSheet on React " +
|
|
110
|
+
'Native\'s default keyboardShouldPersistTaps="never" (sheet content stays in the screen\'s ' +
|
|
111
|
+
"React tree even though it is drawn in another window). Set " +
|
|
112
|
+
'keyboardShouldPersistTaps="always" or "handled" on scroll views that contain the ' +
|
|
113
|
+
"BottomSheet, or wrap the screen in DismissKeyboard.";
|
|
89
114
|
const BottomSheetContext = createContext(null);
|
|
90
115
|
function useBottomSheetContext() {
|
|
91
116
|
const context = use(BottomSheetContext);
|
|
@@ -95,23 +120,23 @@ function useBottomSheetContext() {
|
|
|
95
120
|
return context;
|
|
96
121
|
}
|
|
97
122
|
/**
|
|
98
|
-
* Safe-area insets for content *inside* the sheet.
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* the
|
|
123
|
+
* Safe-area insets for content *inside* the sheet. The native sheet (SwiftUI
|
|
124
|
+
* `.sheet()` / Material `ModalBottomSheet`) is presented outside the React
|
|
125
|
+
* tree's `SafeAreaProvider`, and inside a tab screen the provider context even
|
|
126
|
+
* carries the tab bar (83 pt on an iPhone with a home indicator), so the raw
|
|
127
|
+
* context is the wrong number here. `useWindowInsets` gives the window's
|
|
128
|
+
* insets; `top` sizes the iOS detent cap below. Both native hosts already lay
|
|
129
|
+
* the column out inside the safe area — SwiftUI keeps `RNHostView` 34 pt above
|
|
130
|
+
* the home indicator, and Material3's `ModalBottomSheet` applies the system
|
|
131
|
+
* bars as `contentWindowInsets`, so on an edge-to-edge Pixel the hosted column
|
|
132
|
+
* ends exactly at the navigation bar's top — so `bottom` is 0 on iOS and
|
|
133
|
+
* Android: padding it again pushed the footer up by the inset (measured 64 dp
|
|
134
|
+
* under Save instead of 40 on a Pixel 6a) and squeezed the body. Web is the
|
|
135
|
+
* kit's own overlay inside the React tree and keeps the window's bottom inset.
|
|
105
136
|
*/
|
|
106
137
|
function useSheetInsets() {
|
|
107
|
-
const insets =
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
top: insets.top || fallback?.top || 0,
|
|
111
|
-
bottom: insets.bottom || fallback?.bottom || 0,
|
|
112
|
-
left: insets.left || fallback?.left || 0,
|
|
113
|
-
right: insets.right || fallback?.right || 0,
|
|
114
|
-
};
|
|
138
|
+
const insets = useWindowInsets();
|
|
139
|
+
return Platform.OS === "web" ? insets : { ...insets, bottom: 0 };
|
|
115
140
|
}
|
|
116
141
|
/**
|
|
117
142
|
* Interactive (pull-down / backdrop) dismiss is off only when the consumer
|
|
@@ -147,7 +172,7 @@ function SheetCloseButton({ style }) {
|
|
|
147
172
|
{
|
|
148
173
|
width: spacing.xl,
|
|
149
174
|
height: spacing.xl,
|
|
150
|
-
borderRadius: spacing.
|
|
175
|
+
borderRadius: spacing.radiusFull,
|
|
151
176
|
alignItems: "center",
|
|
152
177
|
justifyContent: "center",
|
|
153
178
|
// Higher-contrast than `muted`: a solid `secondary` fill with a
|
|
@@ -163,7 +188,7 @@ function SheetCloseButton({ style }) {
|
|
|
163
188
|
// ============================================================================
|
|
164
189
|
// Root
|
|
165
190
|
// ============================================================================
|
|
166
|
-
function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, snapPoints = DEFAULT_SNAP_POINTS, closeOnBackdropPress = true, children, }) {
|
|
191
|
+
function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, onDismissed, defaultOpen = false, snapPoints = DEFAULT_SNAP_POINTS, closeOnBackdropPress = true, children, }) {
|
|
167
192
|
const maxSnapIndex = Math.max(snapPoints.length - 1, 0);
|
|
168
193
|
const [state, dispatch] = useReducer(bottomSheetRootReducer, undefined, () => createInitialRootState(defaultOpen, maxSnapIndex));
|
|
169
194
|
const isControlled = controlledOpen !== undefined;
|
|
@@ -188,6 +213,16 @@ function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenC
|
|
|
188
213
|
}
|
|
189
214
|
}, [controlledOnOpenChange, isControlled, maxSnapIndex]);
|
|
190
215
|
const toggle = useCallback(() => onOpenChange(!open), [onOpenChange, open]);
|
|
216
|
+
// Latest callback without re-creating the context value on every render.
|
|
217
|
+
// Synced after each commit (the sheet reports its dismissal from a native
|
|
218
|
+
// or DOM event, never during render) instead of written during render.
|
|
219
|
+
const onDismissedRef = useRef(onDismissed);
|
|
220
|
+
useLayoutEffect(() => {
|
|
221
|
+
onDismissedRef.current = onDismissed;
|
|
222
|
+
});
|
|
223
|
+
const notifyDismissed = useCallback(() => {
|
|
224
|
+
onDismissedRef.current?.();
|
|
225
|
+
}, []);
|
|
191
226
|
const setScrollable = useCallback((scrollable) => {
|
|
192
227
|
dispatch({ type: "setScrollable", scrollable });
|
|
193
228
|
}, []);
|
|
@@ -212,6 +247,7 @@ function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenC
|
|
|
212
247
|
setHasHeader,
|
|
213
248
|
hasFooter: state.hasFooter,
|
|
214
249
|
setHasFooter,
|
|
250
|
+
notifyDismissed,
|
|
215
251
|
}), [
|
|
216
252
|
open,
|
|
217
253
|
onOpenChange,
|
|
@@ -227,6 +263,7 @@ function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenC
|
|
|
227
263
|
setHasHeader,
|
|
228
264
|
state.hasFooter,
|
|
229
265
|
setHasFooter,
|
|
266
|
+
notifyDismissed,
|
|
230
267
|
]);
|
|
231
268
|
return (_jsx(BottomSheetContext.Provider, { value: contextValue, children: children }));
|
|
232
269
|
}
|
|
@@ -254,11 +291,15 @@ function BottomSheetTrigger({ asChild, children, style: styleOverride }) {
|
|
|
254
291
|
function BottomSheetContent({
|
|
255
292
|
// Accepted-but-ignored ergonomics props (platform owns these behaviors):
|
|
256
293
|
swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, testID, children, }) {
|
|
257
|
-
const { open, onOpenChange, snapPoints, snapIndex, setSnapIndex, hasHeader } = useBottomSheetContext();
|
|
294
|
+
const { open, onOpenChange, snapPoints, snapIndex, setSnapIndex, hasHeader, notifyDismissed } = useBottomSheetContext();
|
|
258
295
|
const { theme } = useTheme();
|
|
296
|
+
const sheetShape = useShape("sheet");
|
|
259
297
|
const dismissDisabled = useDismissDisabled();
|
|
260
298
|
const showClose = useShowClose();
|
|
261
299
|
const { height: winH } = useWindowDimensions();
|
|
300
|
+
// Window insets (not the provider context, which reads zero inside the
|
|
301
|
+
// native sheet's window): the top inset sizes the iOS detent cap below.
|
|
302
|
+
const insets = useSheetInsets();
|
|
262
303
|
// Tap-away keyboard dismissal inside the sheet. The native sheet (SwiftUI
|
|
263
304
|
// `.sheet()` / Material `ModalBottomSheet`) hosts its RN children in a separate
|
|
264
305
|
// native window via @expo/ui's `RNHostView`, OUTSIDE the app's
|
|
@@ -272,6 +313,15 @@ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDra
|
|
|
272
313
|
// dead-space tap dismisses on release only, through the registered field's
|
|
273
314
|
// own window-independent blur handle.
|
|
274
315
|
const dismissResponderProps = useKeyboardDismissResponder();
|
|
316
|
+
// Android, dev, once per session: name an ancestor ScrollView that claimed a
|
|
317
|
+
// tap inside the sheet in the capture phase (nothing in here can preempt it).
|
|
318
|
+
const columnResponderProps = useAncestorClaimWarning(dismissResponderProps, bottomSheetAncestorClaimWarning);
|
|
319
|
+
// Android only, dev only, once per session: name the host floor the column's
|
|
320
|
+
// keyboard avoidance depends on when the app was built against an
|
|
321
|
+
// `expo-modules-core` that drops `RNHostView`'s size update (see the header).
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
warnIfBottomSheetHostDropsResize();
|
|
324
|
+
}, []);
|
|
275
325
|
// Boolean open → native imperative index. The root resets snapIndex to the
|
|
276
326
|
// highest point while closed; -1 keeps the native sheet closed.
|
|
277
327
|
const index = open ? snapIndex : -1;
|
|
@@ -287,18 +337,35 @@ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDra
|
|
|
287
337
|
// and Material's `ModalBottomSheet` ignores percentage snap points
|
|
288
338
|
// (partial / expanded only) — a window-percentage cap would leave the Body
|
|
289
339
|
// short of the rendered sheet with a blank strip below it. Android therefore
|
|
290
|
-
// keeps `flex:1` alone (see the content column style).
|
|
340
|
+
// keeps `flex:1` alone (see the content column style). The same measured
|
|
341
|
+
// height is how the column follows the keyboard: Material shrinks the sheet
|
|
342
|
+
// with `imePadding()` and `RNHostView` re-reports its Compose size to the
|
|
343
|
+
// shadow tree, which needs `expo-modules-core` >= 57.0.4 to always land
|
|
344
|
+
// (expo/expo#47778) — see `warnIfBottomSheetHostDropsResize` above.
|
|
345
|
+
//
|
|
346
|
+
// The cap has to be the height SwiftUI really gives the sheet, not a share
|
|
347
|
+
// of the window. `@expo/ui` maps "50%" to `.fraction(0.5)`, a fraction of
|
|
348
|
+
// the *maximum* sheet height — the window minus the top safe-area inset
|
|
349
|
+
// minus UIKit's 10 pt gap above a `.large` sheet — and a numeric snap to
|
|
350
|
+
// `.height(n)`, clamped to that maximum. `@expo/ui` then pads the hosted
|
|
351
|
+
// column 16 pt below the native grabber whenever that grabber is shown. A
|
|
352
|
+
// cap of `fraction × window height` overshot by `fraction × (inset + 10) +
|
|
353
|
+
// 16` ≈ 52 pt on a Dynamic Island phone at 50 %, so the sheet clipped its
|
|
354
|
+
// last row on every such device.
|
|
355
|
+
const hasInteractiveHandle = containsHandle(children);
|
|
291
356
|
const expandedSnap = snapPoints[snapPoints.length - 1];
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
357
|
+
const availableSheetHeight = winH - insets.top - IOS_SHEET_TOP_GAP;
|
|
358
|
+
const sheetHeight = typeof expandedSnap === "number"
|
|
359
|
+
? Math.min(expandedSnap, availableSheetHeight)
|
|
360
|
+
: (parseFloat(expandedSnap) / 100) * availableSheetHeight;
|
|
361
|
+
const detentHeight = sheetHeight - (hasInteractiveHandle ? 0 : IOS_HOST_HANDLE_PADDING);
|
|
295
362
|
// When there's no Header to host the X (so a close affordance is wanted —
|
|
296
363
|
// dismiss is off, or the body scrolls), float one over the top-right corner.
|
|
297
364
|
// A Header, when present, renders its own (see BottomSheetHeader).
|
|
298
365
|
const showFloatingClose = showClose && !hasHeader;
|
|
299
|
-
const hasInteractiveHandle = containsHandle(children);
|
|
300
366
|
const handleChange = (newIndex) => {
|
|
301
367
|
// Native fires onChange(-1) on dismiss (swipe / backdrop / back button).
|
|
368
|
+
// The same close also reaches `handleNativeClose`, which owns `onDismissed`.
|
|
302
369
|
if (newIndex < 0) {
|
|
303
370
|
if (open)
|
|
304
371
|
onOpenChange(false);
|
|
@@ -306,30 +373,67 @@ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDra
|
|
|
306
373
|
}
|
|
307
374
|
setSnapIndex(newIndex);
|
|
308
375
|
};
|
|
376
|
+
// `onDismissed`: once per close. `@expo/ui` fires `onClose` once per close
|
|
377
|
+
// itself (post-dismissal on iOS and Android, see the header); the guard here
|
|
378
|
+
// is defensive and is re-armed when the sheet opens again. On web the close
|
|
379
|
+
// callback precedes the exit animation, so completion is the enclosing
|
|
380
|
+
// `<dialog>`'s `close` event, found from the content column's DOM node.
|
|
381
|
+
const columnRef = useRef(null);
|
|
382
|
+
const dismissedRef = useRef(false);
|
|
383
|
+
const cancelDismissWaitRef = useRef(null);
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
if (open)
|
|
386
|
+
dismissedRef.current = false;
|
|
387
|
+
}, [open]);
|
|
388
|
+
useEffect(() => () => {
|
|
389
|
+
cancelDismissWaitRef.current?.();
|
|
390
|
+
cancelDismissWaitRef.current = null;
|
|
391
|
+
}, []);
|
|
392
|
+
const handleNativeClose = () => {
|
|
393
|
+
if (open)
|
|
394
|
+
onOpenChange(false);
|
|
395
|
+
if (dismissedRef.current)
|
|
396
|
+
return;
|
|
397
|
+
dismissedRef.current = true;
|
|
398
|
+
if (Platform.OS !== "web") {
|
|
399
|
+
notifyDismissed();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
cancelDismissWaitRef.current?.();
|
|
403
|
+
cancelDismissWaitRef.current = awaitDialogClose(columnRef.current, () => {
|
|
404
|
+
cancelDismissWaitRef.current = null;
|
|
405
|
+
notifyDismissed();
|
|
406
|
+
});
|
|
407
|
+
};
|
|
309
408
|
return (_jsx(NativeBottomSheet, { index: index, snapPoints: snapPoints,
|
|
310
409
|
// The native indicator lives outside the hosted RN tree and cannot
|
|
311
410
|
// receive JS presses. Hide it only when the compound Handle supplies the
|
|
312
411
|
// interactive replacement; sheets without Handle retain native chrome.
|
|
313
|
-
handleComponent: hasInteractiveHandle ? null : undefined, enablePanDownToClose: !dismissDisabled, onChange: handleChange, onClose:
|
|
314
|
-
if (open)
|
|
315
|
-
onOpenChange(false);
|
|
316
|
-
},
|
|
412
|
+
handleComponent: hasInteractiveHandle ? null : undefined, enablePanDownToClose: !dismissDisabled, onChange: handleChange, onClose: handleNativeClose,
|
|
317
413
|
// Themes the scrim/background on web (vaul), Android (containerColor),
|
|
318
414
|
// and iOS (presentationBackground). Flattened so native readers that
|
|
319
415
|
// expect a plain object (not a style array) keep working.
|
|
320
416
|
backgroundStyle: StyleSheet.flatten([
|
|
321
417
|
{ backgroundColor: theme.colors.card },
|
|
418
|
+
// Host-app `setShape({ sheet })`: the top corners, where the platform
|
|
419
|
+
// lets the sheet draw them (web's drawer, Android). iOS system sheets
|
|
420
|
+
// keep the system corner radius.
|
|
421
|
+
sheetShape?.borderRadius !== undefined && {
|
|
422
|
+
borderTopLeftRadius: sheetShape.borderRadius,
|
|
423
|
+
borderTopRightRadius: sheetShape.borderRadius,
|
|
424
|
+
},
|
|
322
425
|
backgroundStyleOverride,
|
|
323
|
-
]), children: _jsx(TextColorContext.Provider, { value: theme.colors.foreground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(View, { testID: testID, style: [
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
426
|
+
]), children: _jsx(TextColorContext.Provider, { value: theme.colors.foreground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(View, { ref: columnRef, testID: testID, style: [
|
|
427
|
+
// No fill of its own: the native surface (`backgroundStyle`) is
|
|
428
|
+
// the sheet's only background. On iOS the hosted column starts
|
|
429
|
+
// 16 pt below the sheet's top edge when the native grabber is
|
|
430
|
+
// shown, so a second, column-level fill made a translucent card
|
|
431
|
+
// read as two layers below that line and one above it — a
|
|
432
|
+
// differently colored strip behind the grabber.
|
|
433
|
+
{ flex: 1 },
|
|
330
434
|
Platform.OS !== "android" && { maxHeight: detentHeight },
|
|
331
435
|
styleOverride,
|
|
332
|
-
], ...
|
|
436
|
+
], ...columnResponderProps, children: [children, showFloatingClose && (_jsx(SheetCloseButton, { style: {
|
|
333
437
|
// The sheet card sits below the status bar, so no top inset
|
|
334
438
|
// is needed — just clear the sheet's own top edge / handle.
|
|
335
439
|
position: "absolute",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { ComponentType } from "react";
|
|
2
|
-
import { PressableProps, PressableStateCallbackType, DimensionValue, StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
3
|
-
import { TextProps } from "./StyledText";
|
|
4
|
-
import { type IconProps } from "./Icon";
|
|
2
|
+
import { Pressable, PressableProps, PressableStateCallbackType, DimensionValue, StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
3
|
+
import { TextProps } from "./StyledText.js";
|
|
4
|
+
import { type IconProps } from "./Icon.js";
|
|
5
5
|
/**
|
|
6
6
|
* Button variants
|
|
7
7
|
*/
|
|
@@ -27,6 +27,12 @@ export interface ButtonAccessoryProps {
|
|
|
27
27
|
disabled?: boolean;
|
|
28
28
|
}
|
|
29
29
|
export interface ButtonProps extends PressableProps {
|
|
30
|
+
/**
|
|
31
|
+
* Reaches the underlying `Pressable`. React 19 passes `ref` as a prop; the
|
|
32
|
+
* button composes it with its own host ref (web `aria-disabled`), so
|
|
33
|
+
* `asChild` slots and consumers keep theirs.
|
|
34
|
+
*/
|
|
35
|
+
ref?: React.Ref<React.ComponentRef<typeof Pressable>>;
|
|
30
36
|
/**
|
|
31
37
|
* Text which is looked up via i18n.
|
|
32
38
|
*/
|
|
@@ -82,13 +88,23 @@ export interface ButtonProps extends PressableProps {
|
|
|
82
88
|
*/
|
|
83
89
|
children?: React.ReactNode;
|
|
84
90
|
/**
|
|
85
|
-
*
|
|
91
|
+
* Blocks presses: no `onPress`, no scale, no haptic, dimmed, and out of the
|
|
92
|
+
* web tab order. To announce and dim a button that must stay pressable so
|
|
93
|
+
* the press can say why the action is unavailable ("Add a title first"),
|
|
94
|
+
* pass `aria-disabled` (or `accessibilityState={{ disabled: true }}`)
|
|
95
|
+
* instead — ARIA's contract: announced as disabled, still focusable, still
|
|
96
|
+
* fires `onPress`.
|
|
86
97
|
*/
|
|
87
98
|
disabled?: boolean;
|
|
88
99
|
/**
|
|
89
100
|
* An optional style override for the disabled state
|
|
90
101
|
*/
|
|
91
102
|
disabledStyle?: StyleProp<ViewStyle>;
|
|
103
|
+
/**
|
|
104
|
+
* Haptic on press-in. Omit it to follow the provider-level `haptics` setting
|
|
105
|
+
* (a light tap only under `"all"`); `true` always taps, `false` never does.
|
|
106
|
+
*/
|
|
107
|
+
haptic?: boolean;
|
|
92
108
|
/**
|
|
93
109
|
* Whether to show shadow.
|
|
94
110
|
* @default true for the `default` preset, `false` for every other preset.
|