@mrmeg/expo-ui 0.27.1 → 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 +598 -0
- package/LLM_USAGE.md +233 -35
- package/README.md +504 -103
- 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 +29 -1
- package/dist/components/BottomSheet.js +107 -30
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +60 -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 +87 -16
- 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 +5 -2
- 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 +73 -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 +20 -30
- 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 +71 -26
- 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/index.d.ts +46 -44
- package/dist/components/index.js +2 -0
- package/dist/components/keyboardDismiss.js +12 -4
- 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 -5
- package/dist/lib/index.js +1 -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/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 };
|
|
@@ -61,6 +61,18 @@ import { ViewProps, StyleProp, ViewStyle, ScrollViewProps } from "react-native";
|
|
|
61
61
|
* `keyboardShouldPersistTaps="always"` / `"handled"` on scroll views that
|
|
62
62
|
* contain a sheet, or `DismissKeyboard`, which already sets it.
|
|
63
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`).
|
|
75
|
+
*
|
|
64
76
|
* Scrollable bodies: the native sheet doesn't bound the hosted RN content to
|
|
65
77
|
* the detent height, so a tall `Body` overflows and clips its footer/tail. When
|
|
66
78
|
* `Body` detects overflow, `Content` caps the column to the detent height so the
|
|
@@ -116,12 +128,28 @@ interface BottomSheetContextValue {
|
|
|
116
128
|
/** Whether a `Footer` is mounted, so `Body` can own the bottom safe-area inset when it isn't. */
|
|
117
129
|
hasFooter: boolean;
|
|
118
130
|
setHasFooter: (present: boolean) => void;
|
|
131
|
+
/** Called by `Content` once the native sheet has fully dismissed; invokes the root's `onDismissed`. */
|
|
132
|
+
notifyDismissed: () => void;
|
|
119
133
|
}
|
|
120
134
|
interface BottomSheetProps {
|
|
121
135
|
/** Controlled open state. Omit to use uncontrolled mode with `defaultOpen`. */
|
|
122
136
|
open?: boolean;
|
|
123
137
|
/** Callback when open state changes. */
|
|
124
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;
|
|
125
153
|
/** Initial open state for uncontrolled mode. Default: false. */
|
|
126
154
|
defaultOpen?: boolean;
|
|
127
155
|
/** Snap point heights (px or percentage strings). Default: ["50%"]. */
|
|
@@ -195,7 +223,7 @@ interface BottomSheetCloseProps {
|
|
|
195
223
|
*/
|
|
196
224
|
export declare const bottomSheetAncestorClaimWarning: string;
|
|
197
225
|
declare function useBottomSheetContext(): BottomSheetContextValue;
|
|
198
|
-
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;
|
|
199
227
|
declare function BottomSheetTrigger({ asChild, children, style: styleOverride }: BottomSheetTriggerProps): React.JSX.Element;
|
|
200
228
|
declare function BottomSheetContent({ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, testID, children, }: BottomSheetContentProps): React.JSX.Element;
|
|
201
229
|
declare function BottomSheetHandle({ style }: BottomSheetHandleProps): React.JSX.Element;
|
|
@@ -1,16 +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
12
|
import { useAncestorClaimWarning, useKeyboardDismissResponder } from "./keyboardDismiss.js";
|
|
12
13
|
import { warnIfBottomSheetHostDropsResize } from "./bottomSheetHostSupport.js";
|
|
14
|
+
import { awaitDialogClose } from "./bottomSheetDismiss.js";
|
|
13
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;
|
|
14
26
|
function clampSnapIndex(index, maxSnapIndex) {
|
|
15
27
|
return Math.min(Math.max(index, 0), maxSnapIndex);
|
|
16
28
|
}
|
|
@@ -108,23 +120,23 @@ function useBottomSheetContext() {
|
|
|
108
120
|
return context;
|
|
109
121
|
}
|
|
110
122
|
/**
|
|
111
|
-
* Safe-area insets for content *inside* the sheet.
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* 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.
|
|
118
136
|
*/
|
|
119
137
|
function useSheetInsets() {
|
|
120
|
-
const insets =
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
top: insets.top || fallback?.top || 0,
|
|
124
|
-
bottom: insets.bottom || fallback?.bottom || 0,
|
|
125
|
-
left: insets.left || fallback?.left || 0,
|
|
126
|
-
right: insets.right || fallback?.right || 0,
|
|
127
|
-
};
|
|
138
|
+
const insets = useWindowInsets();
|
|
139
|
+
return Platform.OS === "web" ? insets : { ...insets, bottom: 0 };
|
|
128
140
|
}
|
|
129
141
|
/**
|
|
130
142
|
* Interactive (pull-down / backdrop) dismiss is off only when the consumer
|
|
@@ -160,7 +172,7 @@ function SheetCloseButton({ style }) {
|
|
|
160
172
|
{
|
|
161
173
|
width: spacing.xl,
|
|
162
174
|
height: spacing.xl,
|
|
163
|
-
borderRadius: spacing.
|
|
175
|
+
borderRadius: spacing.radiusFull,
|
|
164
176
|
alignItems: "center",
|
|
165
177
|
justifyContent: "center",
|
|
166
178
|
// Higher-contrast than `muted`: a solid `secondary` fill with a
|
|
@@ -176,7 +188,7 @@ function SheetCloseButton({ style }) {
|
|
|
176
188
|
// ============================================================================
|
|
177
189
|
// Root
|
|
178
190
|
// ============================================================================
|
|
179
|
-
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, }) {
|
|
180
192
|
const maxSnapIndex = Math.max(snapPoints.length - 1, 0);
|
|
181
193
|
const [state, dispatch] = useReducer(bottomSheetRootReducer, undefined, () => createInitialRootState(defaultOpen, maxSnapIndex));
|
|
182
194
|
const isControlled = controlledOpen !== undefined;
|
|
@@ -201,6 +213,16 @@ function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenC
|
|
|
201
213
|
}
|
|
202
214
|
}, [controlledOnOpenChange, isControlled, maxSnapIndex]);
|
|
203
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
|
+
}, []);
|
|
204
226
|
const setScrollable = useCallback((scrollable) => {
|
|
205
227
|
dispatch({ type: "setScrollable", scrollable });
|
|
206
228
|
}, []);
|
|
@@ -225,6 +247,7 @@ function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenC
|
|
|
225
247
|
setHasHeader,
|
|
226
248
|
hasFooter: state.hasFooter,
|
|
227
249
|
setHasFooter,
|
|
250
|
+
notifyDismissed,
|
|
228
251
|
}), [
|
|
229
252
|
open,
|
|
230
253
|
onOpenChange,
|
|
@@ -240,6 +263,7 @@ function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenC
|
|
|
240
263
|
setHasHeader,
|
|
241
264
|
state.hasFooter,
|
|
242
265
|
setHasFooter,
|
|
266
|
+
notifyDismissed,
|
|
243
267
|
]);
|
|
244
268
|
return (_jsx(BottomSheetContext.Provider, { value: contextValue, children: children }));
|
|
245
269
|
}
|
|
@@ -267,11 +291,15 @@ function BottomSheetTrigger({ asChild, children, style: styleOverride }) {
|
|
|
267
291
|
function BottomSheetContent({
|
|
268
292
|
// Accepted-but-ignored ergonomics props (platform owns these behaviors):
|
|
269
293
|
swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDrag: _dismissKeyboardOnDrag, backgroundStyle: backgroundStyleOverride, style: styleOverride, testID, children, }) {
|
|
270
|
-
const { open, onOpenChange, snapPoints, snapIndex, setSnapIndex, hasHeader } = useBottomSheetContext();
|
|
294
|
+
const { open, onOpenChange, snapPoints, snapIndex, setSnapIndex, hasHeader, notifyDismissed } = useBottomSheetContext();
|
|
271
295
|
const { theme } = useTheme();
|
|
296
|
+
const sheetShape = useShape("sheet");
|
|
272
297
|
const dismissDisabled = useDismissDisabled();
|
|
273
298
|
const showClose = useShowClose();
|
|
274
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();
|
|
275
303
|
// Tap-away keyboard dismissal inside the sheet. The native sheet (SwiftUI
|
|
276
304
|
// `.sheet()` / Material `ModalBottomSheet`) hosts its RN children in a separate
|
|
277
305
|
// native window via @expo/ui's `RNHostView`, OUTSIDE the app's
|
|
@@ -314,17 +342,30 @@ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDra
|
|
|
314
342
|
// with `imePadding()` and `RNHostView` re-reports its Compose size to the
|
|
315
343
|
// shadow tree, which needs `expo-modules-core` >= 57.0.4 to always land
|
|
316
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);
|
|
317
356
|
const expandedSnap = snapPoints[snapPoints.length - 1];
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
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);
|
|
321
362
|
// When there's no Header to host the X (so a close affordance is wanted —
|
|
322
363
|
// dismiss is off, or the body scrolls), float one over the top-right corner.
|
|
323
364
|
// A Header, when present, renders its own (see BottomSheetHeader).
|
|
324
365
|
const showFloatingClose = showClose && !hasHeader;
|
|
325
|
-
const hasInteractiveHandle = containsHandle(children);
|
|
326
366
|
const handleChange = (newIndex) => {
|
|
327
367
|
// Native fires onChange(-1) on dismiss (swipe / backdrop / back button).
|
|
368
|
+
// The same close also reaches `handleNativeClose`, which owns `onDismissed`.
|
|
328
369
|
if (newIndex < 0) {
|
|
329
370
|
if (open)
|
|
330
371
|
onOpenChange(false);
|
|
@@ -332,21 +373,57 @@ swipeEnabled: _swipeEnabled, avoidKeyboard: _avoidKeyboard, dismissKeyboardOnDra
|
|
|
332
373
|
}
|
|
333
374
|
setSnapIndex(newIndex);
|
|
334
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
|
+
};
|
|
335
408
|
return (_jsx(NativeBottomSheet, { index: index, snapPoints: snapPoints,
|
|
336
409
|
// The native indicator lives outside the hosted RN tree and cannot
|
|
337
410
|
// receive JS presses. Hide it only when the compound Handle supplies the
|
|
338
411
|
// interactive replacement; sheets without Handle retain native chrome.
|
|
339
|
-
handleComponent: hasInteractiveHandle ? null : undefined, enablePanDownToClose: !dismissDisabled, onChange: handleChange, onClose:
|
|
340
|
-
if (open)
|
|
341
|
-
onOpenChange(false);
|
|
342
|
-
},
|
|
412
|
+
handleComponent: hasInteractiveHandle ? null : undefined, enablePanDownToClose: !dismissDisabled, onChange: handleChange, onClose: handleNativeClose,
|
|
343
413
|
// Themes the scrim/background on web (vaul), Android (containerColor),
|
|
344
414
|
// and iOS (presentationBackground). Flattened so native readers that
|
|
345
415
|
// expect a plain object (not a style array) keep working.
|
|
346
416
|
backgroundStyle: StyleSheet.flatten([
|
|
347
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
|
+
},
|
|
348
425
|
backgroundStyleOverride,
|
|
349
|
-
]), children: _jsx(TextColorContext.Provider, { value: theme.colors.foreground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(View, { testID: testID, style: [
|
|
426
|
+
]), children: _jsx(TextColorContext.Provider, { value: theme.colors.foreground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(View, { ref: columnRef, testID: testID, style: [
|
|
350
427
|
// No fill of its own: the native surface (`backgroundStyle`) is
|
|
351
428
|
// the sheet's only background. On iOS the hosted column starts
|
|
352
429
|
// 16 pt below the sheet's top edge when the native grabber is
|
|
@@ -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.
|