@mrmeg/expo-ui 0.13.0-rsd.0 → 0.13.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 +31 -0
- package/LLM_USAGE.md +15 -0
- package/README.md +15 -1
- package/dist/components/Accordion.d.ts +18 -38
- package/dist/components/Accordion.js +121 -137
- package/dist/components/AnimatedView.d.ts +24 -13
- package/dist/components/AnimatedView.js +29 -70
- package/dist/components/Badge.d.ts +2 -2
- package/dist/components/Badge.js +47 -45
- package/dist/components/BottomSheet.d.ts +73 -30
- package/dist/components/BottomSheet.js +225 -191
- package/dist/components/Button.d.ts +56 -43
- package/dist/components/Button.js +203 -189
- package/dist/components/Card.d.ts +10 -7
- package/dist/components/Card.js +105 -90
- package/dist/components/Checkbox.d.ts +8 -20
- package/dist/components/Checkbox.js +80 -77
- package/dist/components/Collapsible.d.ts +47 -47
- package/dist/components/Collapsible.js +29 -108
- package/dist/components/Dialog.d.ts +121 -80
- package/dist/components/Dialog.js +132 -225
- package/dist/components/DismissKeyboard.js +2 -33
- package/dist/components/Drawer.d.ts +83 -37
- package/dist/components/Drawer.js +512 -252
- package/dist/components/DropdownMenu.d.ts +113 -106
- package/dist/components/DropdownMenu.js +204 -350
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +34 -41
- package/dist/components/InputOTP.d.ts +53 -19
- package/dist/components/InputOTP.js +102 -81
- package/dist/components/KeyboardAvoidingView.d.ts +3 -6
- package/dist/components/KeyboardAvoidingView.js +5 -18
- package/dist/components/Label.d.ts +17 -12
- package/dist/components/Label.js +51 -38
- package/dist/components/MaxWidthContainer.d.ts +16 -5
- package/dist/components/MaxWidthContainer.js +29 -28
- package/dist/components/Notification.d.ts +9 -5
- package/dist/components/Notification.js +187 -190
- package/dist/components/Popover.d.ts +66 -38
- package/dist/components/Popover.js +69 -158
- package/dist/components/Progress.d.ts +4 -5
- package/dist/components/Progress.js +77 -65
- package/dist/components/RadioGroup.d.ts +24 -30
- package/dist/components/RadioGroup.js +94 -90
- package/dist/components/Select.d.ts +74 -62
- package/dist/components/Select.js +154 -241
- package/dist/components/Separator.d.ts +23 -13
- package/dist/components/Separator.js +36 -29
- package/dist/components/Skeleton.d.ts +7 -8
- package/dist/components/Skeleton.js +61 -74
- package/dist/components/StyledText.context.d.ts +2 -6
- package/dist/components/StyledText.context.js +0 -3
- package/dist/components/StyledText.d.ts +7 -29
- package/dist/components/StyledText.js +29 -92
- package/dist/components/Switch.d.ts +6 -18
- package/dist/components/Switch.js +106 -112
- package/dist/components/Tabs.d.ts +16 -26
- package/dist/components/Tabs.js +91 -189
- package/dist/components/TextInput.d.ts +19 -6
- package/dist/components/TextInput.js +198 -226
- package/dist/components/Toggle.d.ts +41 -23
- package/dist/components/Toggle.js +98 -84
- package/dist/components/ToggleGroup.d.ts +29 -37
- package/dist/components/ToggleGroup.js +108 -113
- package/dist/components/Tooltip.d.ts +111 -41
- package/dist/components/Tooltip.js +118 -156
- package/dist/components/UIProvider.d.ts +1 -1
- package/dist/components/UIProvider.js +1 -1
- package/dist/components/keyboardFocusRegistry.d.ts +15 -1
- package/dist/components/keyboardFocusRegistry.js +39 -2
- package/dist/hooks/useTheme.d.ts +10 -34
- package/dist/hooks/useTheme.js +8 -20
- package/dist/lib/index.d.ts +0 -2
- package/dist/lib/index.js +0 -2
- package/package.json +29 -11
- package/dist/lib/portal.d.ts +0 -16
- package/dist/lib/portal.js +0 -84
- package/dist/lib/styles.d.ts +0 -32
- package/dist/lib/styles.js +0 -91
- package/dist/lib/useAnchoredPosition.d.ts +0 -57
- package/dist/lib/useAnchoredPosition.js +0 -120
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import React, { ComponentType } from "react";
|
|
2
|
+
import { PressableProps, PressableStateCallbackType, DimensionValue, StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
2
3
|
import { TextProps } from "./StyledText";
|
|
3
4
|
import { type IconProps } from "./Icon";
|
|
4
|
-
import { type TextStyleValue } from "./StyledText.context";
|
|
5
|
-
import { type LooseStyle } from "../lib/styles";
|
|
6
|
-
/**
|
|
7
|
-
* Press/pointer/focus handlers accept an optional event arg. Kept loose
|
|
8
|
-
* (`any`) so consumers can pass react-native GestureResponderEvent handlers and
|
|
9
|
-
* expo-router's <Link asChild> injected onPress without a type mismatch.
|
|
10
|
-
*/
|
|
11
|
-
type PointerHandler = (event?: any) => void;
|
|
12
5
|
/**
|
|
13
6
|
* Button variants
|
|
14
7
|
*/
|
|
@@ -17,14 +10,23 @@ type Presets = "default" | "outline" | "ghost" | "link" | "destructive" | "secon
|
|
|
17
10
|
* Button size variants
|
|
18
11
|
*/
|
|
19
12
|
export type ButtonSize = "sm" | "md" | "lg";
|
|
20
|
-
export type ButtonAccessoryStyle =
|
|
13
|
+
export type ButtonAccessoryStyle = {
|
|
14
|
+
margin?: DimensionValue;
|
|
15
|
+
marginHorizontal?: DimensionValue;
|
|
16
|
+
marginVertical?: DimensionValue;
|
|
17
|
+
marginTop?: DimensionValue;
|
|
18
|
+
marginRight?: DimensionValue;
|
|
19
|
+
marginBottom?: DimensionValue;
|
|
20
|
+
marginLeft?: DimensionValue;
|
|
21
|
+
marginStart?: DimensionValue;
|
|
22
|
+
marginEnd?: DimensionValue;
|
|
23
|
+
};
|
|
21
24
|
export interface ButtonAccessoryProps {
|
|
22
25
|
style: ButtonAccessoryStyle;
|
|
23
|
-
|
|
24
|
-
pressed: boolean;
|
|
26
|
+
pressableState: PressableStateCallbackType;
|
|
25
27
|
disabled?: boolean;
|
|
26
28
|
}
|
|
27
|
-
export interface ButtonProps {
|
|
29
|
+
export interface ButtonProps extends PressableProps {
|
|
28
30
|
/**
|
|
29
31
|
* Text which is looked up via i18n.
|
|
30
32
|
*/
|
|
@@ -38,38 +40,25 @@ export interface ButtonProps {
|
|
|
38
40
|
*/
|
|
39
41
|
txOptions?: TextProps["txOptions"];
|
|
40
42
|
/**
|
|
41
|
-
*
|
|
43
|
+
* An optional style override useful for padding & margin.
|
|
42
44
|
*/
|
|
43
|
-
|
|
44
|
-
/** Pointer-down handler (maps to onPointerDown). */
|
|
45
|
-
onPressIn?: PointerHandler;
|
|
46
|
-
/** Pointer-up handler (maps to onPointerUp). */
|
|
47
|
-
onPressOut?: PointerHandler;
|
|
48
|
-
/** Focus handler. */
|
|
49
|
-
onFocus?: PointerHandler;
|
|
50
|
-
/** Blur handler. */
|
|
51
|
-
onBlur?: PointerHandler;
|
|
52
|
-
/**
|
|
53
|
-
* An optional style override useful for padding & margin. Accepts
|
|
54
|
-
* react-strict-dom styles or plain CSS-shaped objects during the migration.
|
|
55
|
-
*/
|
|
56
|
-
style?: LooseStyle;
|
|
45
|
+
style?: StyleProp<ViewStyle>;
|
|
57
46
|
/**
|
|
58
47
|
* An optional style override for the "pressed" state.
|
|
59
48
|
*/
|
|
60
|
-
pressedStyle?:
|
|
49
|
+
pressedStyle?: StyleProp<ViewStyle>;
|
|
61
50
|
/**
|
|
62
51
|
* An optional style override for the button text.
|
|
63
52
|
*/
|
|
64
|
-
textStyle?:
|
|
53
|
+
textStyle?: StyleProp<TextStyle>;
|
|
65
54
|
/**
|
|
66
55
|
* An optional style override for the button text when in the "pressed" state.
|
|
67
56
|
*/
|
|
68
|
-
pressedTextStyle?:
|
|
57
|
+
pressedTextStyle?: StyleProp<TextStyle>;
|
|
69
58
|
/**
|
|
70
59
|
* An optional style override for the button text when in the "disabled" state.
|
|
71
60
|
*/
|
|
72
|
-
disabledTextStyle?:
|
|
61
|
+
disabledTextStyle?: StyleProp<TextStyle>;
|
|
73
62
|
/**
|
|
74
63
|
* One of the different types of button presets.
|
|
75
64
|
* @default "default"
|
|
@@ -99,7 +88,7 @@ export interface ButtonProps {
|
|
|
99
88
|
/**
|
|
100
89
|
* An optional style override for the disabled state
|
|
101
90
|
*/
|
|
102
|
-
disabledStyle?:
|
|
91
|
+
disabledStyle?: StyleProp<ViewStyle>;
|
|
103
92
|
/**
|
|
104
93
|
* Whether to show shadow
|
|
105
94
|
*/
|
|
@@ -112,17 +101,9 @@ export interface ButtonProps {
|
|
|
112
101
|
* Whether button should take full width of container
|
|
113
102
|
*/
|
|
114
103
|
fullWidth?: boolean;
|
|
115
|
-
/** Forwarded accessibility label. */
|
|
116
|
-
"aria-label"?: string;
|
|
117
|
-
/** Forwarded test id (also accepts `data-testid`). */
|
|
118
|
-
testID?: string;
|
|
119
|
-
/** Forwarded test id (DOM-style; alias of testID). */
|
|
120
|
-
"data-testid"?: string;
|
|
121
|
-
/** Accessibility role hint (kept for RN-API compatibility; defaults button). */
|
|
122
|
-
accessibilityLabel?: string;
|
|
123
104
|
}
|
|
124
105
|
/**
|
|
125
|
-
* Enhanced Button Component
|
|
106
|
+
* Enhanced Button Component
|
|
126
107
|
*
|
|
127
108
|
* Features:
|
|
128
109
|
* - 6 variants (default, outline, ghost, link, destructive, secondary)
|
|
@@ -135,9 +116,25 @@ export interface ButtonProps {
|
|
|
135
116
|
*
|
|
136
117
|
* Usage:
|
|
137
118
|
* ```tsx
|
|
119
|
+
* // Simplest path — plain label via the `text` prop
|
|
138
120
|
* <Button onPress={handler} text="Click Me" />
|
|
121
|
+
*
|
|
122
|
+
* // Different variants
|
|
139
123
|
* <Button preset="outline" onPress={handler} text="Outline" />
|
|
124
|
+
* <Button preset="ghost" onPress={handler} text="Ghost" />
|
|
125
|
+
* <Button preset="destructive" onPress={handler} text="Delete" />
|
|
126
|
+
*
|
|
127
|
+
* // Different sizes
|
|
128
|
+
* <Button size="sm" onPress={handler} text="Small" />
|
|
129
|
+
* <Button size="lg" onPress={handler} text="Large" />
|
|
130
|
+
*
|
|
131
|
+
* // Loading state
|
|
140
132
|
* <Button loading onPress={handler} text="Processing..." />
|
|
133
|
+
*
|
|
134
|
+
* // Full width
|
|
135
|
+
* <Button fullWidth onPress={handler} text="Submit" />
|
|
136
|
+
*
|
|
137
|
+
* // Composed content — icon + label via subcomponents
|
|
141
138
|
* <Button onPress={handler}>
|
|
142
139
|
* <Button.Icon name="heart" />
|
|
143
140
|
* <Button.Text>Like</Button.Text>
|
|
@@ -148,19 +145,35 @@ declare function ButtonRoot(props: ButtonProps): React.JSX.Element;
|
|
|
148
145
|
/**
|
|
149
146
|
* Button.Text
|
|
150
147
|
* Text content for a Button. Inherits the button's control typography, color,
|
|
151
|
-
* and non-selectable behavior from context
|
|
148
|
+
* and non-selectable behavior from context, so callers state their intent
|
|
149
|
+
* explicitly instead of relying on the component to inspect `typeof children`.
|
|
150
|
+
*
|
|
151
|
+
* ```tsx
|
|
152
|
+
* <Button onPress={save}>
|
|
153
|
+
* <Button.Text>Save</Button.Text>
|
|
154
|
+
* </Button>
|
|
155
|
+
* ```
|
|
152
156
|
*/
|
|
153
157
|
declare function ButtonText(props: TextProps): React.JSX.Element;
|
|
154
158
|
/**
|
|
155
159
|
* Button.Icon
|
|
156
160
|
* Icon content for a Button. Defaults its color to the button's text color
|
|
157
|
-
* (from context) and is decorative by default.
|
|
161
|
+
* (from context) and is decorative by default, since the label conveys meaning.
|
|
162
|
+
*
|
|
163
|
+
* ```tsx
|
|
164
|
+
* <Button onPress={like}>
|
|
165
|
+
* <Button.Icon name="heart" />
|
|
166
|
+
* <Button.Text>Like</Button.Text>
|
|
167
|
+
* </Button>
|
|
168
|
+
* ```
|
|
158
169
|
*/
|
|
159
170
|
declare function ButtonIcon(props: IconProps): React.JSX.Element;
|
|
160
171
|
/**
|
|
161
172
|
* Button with explicit subcomponents.
|
|
162
173
|
* - `Button.Text` for label text (inherits control typography)
|
|
163
174
|
* - `Button.Icon` for icons (inherits the button's text color)
|
|
175
|
+
*
|
|
176
|
+
* The `tx`/`text` props remain the simplest path for plain labels.
|
|
164
177
|
*/
|
|
165
178
|
declare const Button: typeof ButtonRoot & {
|
|
166
179
|
Text: typeof ButtonText;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { use, useCallback, useMemo, useState
|
|
3
|
-
import {
|
|
2
|
+
import { use, useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { Pressable, StyleSheet, View, Platform, ActivityIndicator, Animated, } from "react-native";
|
|
4
4
|
import { spacing } from "../constants/spacing.js";
|
|
5
5
|
import { StyledText } from "./StyledText.js";
|
|
6
6
|
import { Icon } from "./Icon.js";
|
|
7
|
-
import { TextColorContext, TextSelectabilityContext, TextStyleContext
|
|
7
|
+
import { TextColorContext, TextSelectabilityContext, TextStyleContext } from "./StyledText.context";
|
|
8
8
|
import { fontFamilies } from "../constants/fonts.js";
|
|
9
9
|
import { palette } from "../constants/colors.js";
|
|
10
10
|
import { useTheme } from "../hooks/useTheme.js";
|
|
11
|
-
import {
|
|
11
|
+
import { useScalePress } from "../hooks/useScalePress.js";
|
|
12
12
|
const SIZE_CONFIGS = {
|
|
13
13
|
sm: {
|
|
14
14
|
paddingVertical: spacing.xxs,
|
|
@@ -29,120 +29,9 @@ const SIZE_CONFIGS = {
|
|
|
29
29
|
height: 40,
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
|
-
|
|
33
|
-
// plugin (web) can only fold a keyframe name defined in the same file, and it
|
|
34
|
-
// cannot fold a runtime ternary condition. On native css.keyframes is a no-op
|
|
35
|
-
// that just returns its arg (a __DEV__ console log), and the animationName has
|
|
36
|
-
// no effect — the spinner shows a static ring (native animation fidelity gap).
|
|
37
|
-
const spin = css.keyframes({
|
|
38
|
-
from: { transform: "rotate(0deg)" },
|
|
39
|
-
to: { transform: "rotate(360deg)" },
|
|
40
|
-
});
|
|
41
|
-
// Static + function-rule styles. Theme colors and per-size numbers are runtime
|
|
42
|
-
// values carried via function rules; layout/structure is static where possible.
|
|
43
|
-
const styles = css.create({
|
|
44
|
-
pressable: (alignSelf) => ({
|
|
45
|
-
alignSelf,
|
|
46
|
-
appearance: "none",
|
|
47
|
-
backgroundColor: "transparent",
|
|
48
|
-
borderWidth: 0,
|
|
49
|
-
padding: 0,
|
|
50
|
-
margin: 0,
|
|
51
|
-
cursor: "pointer",
|
|
52
|
-
}),
|
|
53
|
-
// StyleX requires values referenced inside css.create() to be statically
|
|
54
|
-
// resolvable, AND css.defineConsts is unusable here (it returns undefined on
|
|
55
|
-
// the react-strict-dom native runtime). So spacing tokens are threaded in as
|
|
56
|
-
// function-rule arguments. borderRadius literal (6) = spacing.radiusMd.
|
|
57
|
-
surface: (paddingVertical, paddingHorizontal, minHeight, borderRadius) => ({
|
|
58
|
-
position: "relative",
|
|
59
|
-
display: "flex",
|
|
60
|
-
flexDirection: "row",
|
|
61
|
-
alignItems: "center",
|
|
62
|
-
justifyContent: "center",
|
|
63
|
-
borderRadius,
|
|
64
|
-
paddingTop: paddingVertical,
|
|
65
|
-
paddingBottom: paddingVertical,
|
|
66
|
-
paddingLeft: paddingHorizontal,
|
|
67
|
-
paddingRight: paddingHorizontal,
|
|
68
|
-
minHeight,
|
|
69
|
-
// NOTE: no flexShrink here. This is the button's OUTERMOST element, whose
|
|
70
|
-
// parent is consumer-controlled and often display:block on web — react-strict-dom
|
|
71
|
-
// warns "display:flex is required on the parent for flexShrink to have an effect."
|
|
72
|
-
// Shrink behaviour in a flex row is the parent's concern, not this leaf's.
|
|
73
|
-
transitionProperty: "transform, opacity",
|
|
74
|
-
transitionDuration: "0.1s",
|
|
75
|
-
}),
|
|
76
|
-
bg: (backgroundColor) => ({ backgroundColor }),
|
|
77
|
-
outline: (borderColor) => ({
|
|
78
|
-
backgroundColor: "transparent",
|
|
79
|
-
borderWidth: 1,
|
|
80
|
-
borderStyle: "solid",
|
|
81
|
-
borderColor,
|
|
82
|
-
}),
|
|
83
|
-
transparentBg: { backgroundColor: "transparent" },
|
|
84
|
-
link: {
|
|
85
|
-
backgroundColor: "transparent",
|
|
86
|
-
paddingTop: 0,
|
|
87
|
-
paddingBottom: 0,
|
|
88
|
-
paddingLeft: 0,
|
|
89
|
-
paddingRight: 0,
|
|
90
|
-
},
|
|
91
|
-
fullWidth: { width: "100%" },
|
|
92
|
-
scalePressed: { transform: "scale(0.97)" },
|
|
93
|
-
pressed: { opacity: 0.9 },
|
|
94
|
-
pressedMuted: (backgroundColor) => ({ backgroundColor }),
|
|
95
|
-
disabled: { opacity: 0.6, cursor: "default" },
|
|
96
|
-
focusRing: (boxShadow) => ({ boxShadow }),
|
|
97
|
-
shadow: (boxShadow) => ({ boxShadow }),
|
|
98
|
-
content: {
|
|
99
|
-
display: "flex",
|
|
100
|
-
flexDirection: "row",
|
|
101
|
-
alignItems: "center",
|
|
102
|
-
justifyContent: "center",
|
|
103
|
-
flexShrink: 0,
|
|
104
|
-
},
|
|
105
|
-
loadingContent: { opacity: 0 },
|
|
106
|
-
loaderOverlay: {
|
|
107
|
-
position: "absolute",
|
|
108
|
-
top: 0,
|
|
109
|
-
left: 0,
|
|
110
|
-
right: 0,
|
|
111
|
-
bottom: 0,
|
|
112
|
-
display: "flex",
|
|
113
|
-
alignItems: "center",
|
|
114
|
-
justifyContent: "center",
|
|
115
|
-
pointerEvents: "none",
|
|
116
|
-
},
|
|
117
|
-
spinner: (color) => ({
|
|
118
|
-
width: 16,
|
|
119
|
-
height: 16,
|
|
120
|
-
borderRadius: 9999,
|
|
121
|
-
borderWidth: 2,
|
|
122
|
-
borderStyle: "solid",
|
|
123
|
-
borderColor: color,
|
|
124
|
-
borderTopColor: "transparent",
|
|
125
|
-
animationName: spin,
|
|
126
|
-
animationDuration: "0.7s",
|
|
127
|
-
animationIterationCount: "infinite",
|
|
128
|
-
animationTimingFunction: "linear",
|
|
129
|
-
}),
|
|
130
|
-
});
|
|
131
|
-
// Text style passed to the label via context. Plain object (function of size).
|
|
132
|
-
function buildTextStyle(size) {
|
|
133
|
-
const sizeConfig = SIZE_CONFIGS[size];
|
|
134
|
-
return {
|
|
135
|
-
fontFamily: fontFamilies.sansSerif.regular,
|
|
136
|
-
fontSize: sizeConfig.fontSize,
|
|
137
|
-
fontWeight: "500",
|
|
138
|
-
textAlign: "center",
|
|
139
|
-
lineHeight: sizeConfig.fontSize * 1.4,
|
|
140
|
-
flexShrink: 0,
|
|
141
|
-
userSelect: "none",
|
|
142
|
-
};
|
|
143
|
-
}
|
|
32
|
+
const getNativeHitSlop = (sizeConfig) => Math.ceil(Math.max(0, spacing.touchTarget - sizeConfig.height) / 2);
|
|
144
33
|
/**
|
|
145
|
-
* Enhanced Button Component
|
|
34
|
+
* Enhanced Button Component
|
|
146
35
|
*
|
|
147
36
|
* Features:
|
|
148
37
|
* - 6 variants (default, outline, ghost, link, destructive, secondary)
|
|
@@ -155,9 +44,25 @@ function buildTextStyle(size) {
|
|
|
155
44
|
*
|
|
156
45
|
* Usage:
|
|
157
46
|
* ```tsx
|
|
47
|
+
* // Simplest path — plain label via the `text` prop
|
|
158
48
|
* <Button onPress={handler} text="Click Me" />
|
|
49
|
+
*
|
|
50
|
+
* // Different variants
|
|
159
51
|
* <Button preset="outline" onPress={handler} text="Outline" />
|
|
52
|
+
* <Button preset="ghost" onPress={handler} text="Ghost" />
|
|
53
|
+
* <Button preset="destructive" onPress={handler} text="Delete" />
|
|
54
|
+
*
|
|
55
|
+
* // Different sizes
|
|
56
|
+
* <Button size="sm" onPress={handler} text="Small" />
|
|
57
|
+
* <Button size="lg" onPress={handler} text="Large" />
|
|
58
|
+
*
|
|
59
|
+
* // Loading state
|
|
160
60
|
* <Button loading onPress={handler} text="Processing..." />
|
|
61
|
+
*
|
|
62
|
+
* // Full width
|
|
63
|
+
* <Button fullWidth onPress={handler} text="Submit" />
|
|
64
|
+
*
|
|
65
|
+
* // Composed content — icon + label via subcomponents
|
|
161
66
|
* <Button onPress={handler}>
|
|
162
67
|
* <Button.Icon name="heart" />
|
|
163
68
|
* <Button.Text>Like</Button.Text>
|
|
@@ -165,17 +70,20 @@ function buildTextStyle(size) {
|
|
|
165
70
|
* ```
|
|
166
71
|
*/
|
|
167
72
|
function ButtonRoot(props) {
|
|
168
|
-
const { tx, text, txOptions, style: styleOverride, pressedStyle: pressedStyleOverride, textStyle: textStyleOverride, pressedTextStyle: pressedTextStyleOverride, disabledTextStyle: disabledTextStyleOverride, children, RightAccessory, LeftAccessory, disabled, disabledStyle: disabledStyleOverride, withShadow = false, preset = "default", size = "md", loading = false, fullWidth = false,
|
|
73
|
+
const { tx, text, txOptions, style: styleOverride, pressedStyle: pressedStyleOverride, textStyle: textStyleOverride, pressedTextStyle: pressedTextStyleOverride, disabledTextStyle: disabledTextStyleOverride, children, RightAccessory, LeftAccessory, disabled, disabledStyle: disabledStyleOverride, withShadow = false, preset = "default", size = "md", loading = false, fullWidth = false, onFocus, onBlur, onPressIn, onPressOut, ...rest } = props;
|
|
169
74
|
const { theme, getContrastingColor, getFocusRingStyle, getShadowStyle } = useTheme();
|
|
170
|
-
const
|
|
75
|
+
const styles = useMemo(() => createStyles(theme, size), [theme, size]);
|
|
171
76
|
const shadowStyle = getShadowStyle("base");
|
|
77
|
+
const sizeConfig = SIZE_CONFIGS[size];
|
|
172
78
|
const focusRingStyle = getFocusRingStyle();
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
const
|
|
79
|
+
// Pre-compute background color for contrast calculation
|
|
80
|
+
// Always flatten to handle both array styles (from Slot) and RegisteredStyle
|
|
81
|
+
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
82
|
+
const customBgColor = flattenedStyle?.backgroundColor;
|
|
83
|
+
// Calculate background color for each preset
|
|
176
84
|
let backgroundColor;
|
|
177
|
-
if (typeof
|
|
178
|
-
backgroundColor =
|
|
85
|
+
if (customBgColor && typeof customBgColor === "string") {
|
|
86
|
+
backgroundColor = customBgColor;
|
|
179
87
|
}
|
|
180
88
|
else if (preset === "default") {
|
|
181
89
|
backgroundColor = theme.colors.primary;
|
|
@@ -192,7 +100,7 @@ function ButtonRoot(props) {
|
|
|
192
100
|
else {
|
|
193
101
|
backgroundColor = theme.colors.primary;
|
|
194
102
|
}
|
|
195
|
-
//
|
|
103
|
+
// Determine text color per preset for readable contrast
|
|
196
104
|
const textColor = preset === "outline"
|
|
197
105
|
? theme.colors.foreground
|
|
198
106
|
: preset === "ghost"
|
|
@@ -205,87 +113,101 @@ function ButtonRoot(props) {
|
|
|
205
113
|
? theme.colors.secondaryForeground
|
|
206
114
|
: getContrastingColor(backgroundColor, palette.white, palette.black);
|
|
207
115
|
const [focused, setFocused] = useState(false);
|
|
208
|
-
const [
|
|
116
|
+
const [restingWidth, setRestingWidth] = useState();
|
|
209
117
|
const isDisabled = disabled || loading;
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
const
|
|
216
|
-
//
|
|
217
|
-
// :focus-visible
|
|
218
|
-
//
|
|
118
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
119
|
+
disabled: !!isDisabled,
|
|
120
|
+
haptic: false,
|
|
121
|
+
scaleTo: preset === "link" ? 1 : 0.97,
|
|
122
|
+
});
|
|
123
|
+
const showFocusRing = (event) => {
|
|
124
|
+
// On web, pointer taps focus the element too, which left the ring visible
|
|
125
|
+
// after every click. :focus-visible is only true for keyboard-driven
|
|
126
|
+
// focus, so gate the ring on it; fall back to showing the ring when the
|
|
127
|
+
// target can't be queried (non-DOM targets, older engines).
|
|
219
128
|
let ringVisible = true;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
129
|
+
if (Platform.OS === "web") {
|
|
130
|
+
const target = event?.nativeEvent?.target;
|
|
131
|
+
if (target && typeof target.matches === "function") {
|
|
132
|
+
try {
|
|
133
|
+
ringVisible = target.matches(":focus-visible");
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
ringVisible = true;
|
|
137
|
+
}
|
|
227
138
|
}
|
|
228
139
|
}
|
|
229
140
|
setFocused(ringVisible);
|
|
230
141
|
onFocus?.(event);
|
|
231
142
|
};
|
|
232
|
-
const
|
|
143
|
+
const hideFocusRing = (event) => {
|
|
233
144
|
setFocused(false);
|
|
234
145
|
onBlur?.(event);
|
|
235
146
|
};
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
return;
|
|
239
|
-
setPressed(true);
|
|
147
|
+
const handlePressIn = (event) => {
|
|
148
|
+
pressHandlers.onPressIn();
|
|
240
149
|
onPressIn?.(event);
|
|
241
150
|
};
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
return;
|
|
245
|
-
setPressed(false);
|
|
151
|
+
const handlePressOut = (event) => {
|
|
152
|
+
pressHandlers.onPressOut();
|
|
246
153
|
onPressOut?.(event);
|
|
247
154
|
};
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
155
|
+
const handleButtonLayout = useCallback((event) => {
|
|
156
|
+
if (loading || fullWidth)
|
|
157
|
+
return;
|
|
158
|
+
const nextWidth = event.nativeEvent.layout.width;
|
|
159
|
+
if (nextWidth <= 0)
|
|
160
|
+
return;
|
|
161
|
+
setRestingWidth((currentWidth) => {
|
|
162
|
+
if (currentWidth !== undefined && Math.abs(currentWidth - nextWidth) < 0.5) {
|
|
163
|
+
return currentWidth;
|
|
164
|
+
}
|
|
165
|
+
return nextWidth;
|
|
166
|
+
});
|
|
167
|
+
}, [fullWidth, loading]);
|
|
168
|
+
return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TextStyleContext.Provider, { value: styles.text, children: _jsx(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: !!isDisabled, busy: loading }, ...rest, onPressIn: handlePressIn, onPressOut: handlePressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: { alignSelf: fullWidth ? "stretch" : flattenedStyle?.alignSelf ?? "flex-start" }, hitSlop: rest.hitSlop ?? (Platform.OS === "web" ? undefined : getNativeHitSlop(sizeConfig)), disabled: isDisabled, children: (state) => (_jsx(Animated.View, { style: scaleStyle, children: _jsxs(View, { style: [
|
|
169
|
+
styles.button,
|
|
170
|
+
preset === "default" && styles.buttonDefault,
|
|
171
|
+
preset === "outline" && styles.buttonOutline,
|
|
172
|
+
preset === "ghost" && styles.buttonGhost,
|
|
173
|
+
preset === "link" && styles.buttonLink,
|
|
174
|
+
preset === "destructive" && styles.buttonDestructive,
|
|
175
|
+
preset === "secondary" && styles.buttonSecondary,
|
|
176
|
+
fullWidth && styles.fullWidth,
|
|
177
|
+
withShadow && !isDisabled && shadowStyle,
|
|
178
|
+
state.pressed && preset === "outline" && styles.pressedMuted,
|
|
179
|
+
state.pressed && preset === "ghost" && styles.pressedMuted,
|
|
180
|
+
state.pressed && styles.pressed,
|
|
181
|
+
state.pressed && pressedStyleOverride,
|
|
182
|
+
isDisabled && styles.disabled,
|
|
183
|
+
isDisabled && disabledStyleOverride,
|
|
184
|
+
focused && !isDisabled && focusRingStyle,
|
|
185
|
+
loading && restingWidth !== undefined && !fullWidth && { width: restingWidth },
|
|
186
|
+
// Spread array styles from Slot to prevent nested arrays on web
|
|
187
|
+
...(Array.isArray(styleOverride) ? styleOverride : [styleOverride]),
|
|
188
|
+
], onLayout: handleButtonLayout, children: [loading && (_jsx(View, { style: [styles.loaderOverlay, { pointerEvents: "none" }], children: _jsx(ActivityIndicator, { size: "small", color: textColor }) })), _jsxs(View, { style: [styles.content, loading && styles.loadingContent, { pointerEvents: loading ? "none" : "auto" }], children: [!!LeftAccessory && (_jsx(LeftAccessory, { style: styles.leftAccessory, pressableState: state, disabled: isDisabled })), (tx || text) ? (_jsx(StyledText, { tx: tx, text: text, txOptions: txOptions, style: [
|
|
189
|
+
styles.text,
|
|
190
|
+
state.pressed && styles.pressedText,
|
|
191
|
+
state.pressed && pressedTextStyleOverride,
|
|
192
|
+
isDisabled && disabledTextStyleOverride,
|
|
193
|
+
textStyleOverride,
|
|
194
|
+
] })) : (
|
|
195
|
+
// Children render as-is. For text content, use the explicit
|
|
196
|
+
// <Button.Text> subcomponent so it inherits control typography
|
|
197
|
+
// via context instead of relying on a runtime `typeof` check.
|
|
198
|
+
children ?? null), !!RightAccessory && (_jsx(RightAccessory, { style: styles.rightAccessory, pressableState: state, disabled: isDisabled }))] })] }) })) }) }) }) }));
|
|
284
199
|
}
|
|
285
200
|
/**
|
|
286
201
|
* Button.Text
|
|
287
202
|
* Text content for a Button. Inherits the button's control typography, color,
|
|
288
|
-
* and non-selectable behavior from context
|
|
203
|
+
* and non-selectable behavior from context, so callers state their intent
|
|
204
|
+
* explicitly instead of relying on the component to inspect `typeof children`.
|
|
205
|
+
*
|
|
206
|
+
* ```tsx
|
|
207
|
+
* <Button onPress={save}>
|
|
208
|
+
* <Button.Text>Save</Button.Text>
|
|
209
|
+
* </Button>
|
|
210
|
+
* ```
|
|
289
211
|
*/
|
|
290
212
|
function ButtonText(props) {
|
|
291
213
|
return _jsx(StyledText, { ...props });
|
|
@@ -293,16 +215,108 @@ function ButtonText(props) {
|
|
|
293
215
|
/**
|
|
294
216
|
* Button.Icon
|
|
295
217
|
* Icon content for a Button. Defaults its color to the button's text color
|
|
296
|
-
* (from context) and is decorative by default.
|
|
218
|
+
* (from context) and is decorative by default, since the label conveys meaning.
|
|
219
|
+
*
|
|
220
|
+
* ```tsx
|
|
221
|
+
* <Button onPress={like}>
|
|
222
|
+
* <Button.Icon name="heart" />
|
|
223
|
+
* <Button.Text>Like</Button.Text>
|
|
224
|
+
* </Button>
|
|
225
|
+
* ```
|
|
297
226
|
*/
|
|
298
227
|
function ButtonIcon(props) {
|
|
299
228
|
const contextColor = use(TextColorContext);
|
|
300
229
|
return _jsx(Icon, { decorative: true, color: contextColor, ...props });
|
|
301
230
|
}
|
|
231
|
+
const createStyles = (theme, size) => {
|
|
232
|
+
const sizeConfig = SIZE_CONFIGS[size];
|
|
233
|
+
return StyleSheet.create({
|
|
234
|
+
button: {
|
|
235
|
+
position: "relative",
|
|
236
|
+
flexDirection: "row",
|
|
237
|
+
alignItems: "center",
|
|
238
|
+
justifyContent: "center",
|
|
239
|
+
borderRadius: spacing.radiusMd,
|
|
240
|
+
paddingVertical: sizeConfig.paddingVertical,
|
|
241
|
+
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
242
|
+
minHeight: sizeConfig.height,
|
|
243
|
+
flexShrink: 0,
|
|
244
|
+
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
245
|
+
},
|
|
246
|
+
content: {
|
|
247
|
+
flexDirection: "row",
|
|
248
|
+
alignItems: "center",
|
|
249
|
+
justifyContent: "center",
|
|
250
|
+
flexShrink: 0,
|
|
251
|
+
},
|
|
252
|
+
loadingContent: {
|
|
253
|
+
opacity: 0,
|
|
254
|
+
},
|
|
255
|
+
buttonDefault: {
|
|
256
|
+
backgroundColor: theme.colors.primary,
|
|
257
|
+
},
|
|
258
|
+
buttonSecondary: {
|
|
259
|
+
backgroundColor: theme.colors.secondary,
|
|
260
|
+
},
|
|
261
|
+
buttonDestructive: {
|
|
262
|
+
backgroundColor: theme.colors.destructive,
|
|
263
|
+
},
|
|
264
|
+
buttonOutline: {
|
|
265
|
+
backgroundColor: "transparent",
|
|
266
|
+
borderWidth: 1,
|
|
267
|
+
borderColor: theme.colors.input,
|
|
268
|
+
},
|
|
269
|
+
buttonGhost: {
|
|
270
|
+
backgroundColor: "transparent",
|
|
271
|
+
},
|
|
272
|
+
buttonLink: {
|
|
273
|
+
backgroundColor: "transparent",
|
|
274
|
+
paddingVertical: 0,
|
|
275
|
+
paddingHorizontal: 0,
|
|
276
|
+
},
|
|
277
|
+
fullWidth: {
|
|
278
|
+
width: "100%",
|
|
279
|
+
},
|
|
280
|
+
text: {
|
|
281
|
+
fontFamily: fontFamilies.sansSerif.regular,
|
|
282
|
+
fontSize: sizeConfig.fontSize,
|
|
283
|
+
fontWeight: "500",
|
|
284
|
+
textAlign: "center",
|
|
285
|
+
lineHeight: sizeConfig.fontSize * 1.4,
|
|
286
|
+
flexShrink: 0,
|
|
287
|
+
userSelect: "none",
|
|
288
|
+
},
|
|
289
|
+
pressed: {
|
|
290
|
+
opacity: 0.9,
|
|
291
|
+
},
|
|
292
|
+
pressedMuted: {
|
|
293
|
+
backgroundColor: theme.colors.muted,
|
|
294
|
+
},
|
|
295
|
+
pressedText: {
|
|
296
|
+
opacity: 0.9,
|
|
297
|
+
},
|
|
298
|
+
disabled: {
|
|
299
|
+
opacity: 0.6,
|
|
300
|
+
},
|
|
301
|
+
leftAccessory: {
|
|
302
|
+
marginRight: spacing.sm,
|
|
303
|
+
},
|
|
304
|
+
rightAccessory: {
|
|
305
|
+
marginLeft: spacing.sm,
|
|
306
|
+
},
|
|
307
|
+
loaderOverlay: {
|
|
308
|
+
...StyleSheet.absoluteFill,
|
|
309
|
+
alignItems: "center",
|
|
310
|
+
justifyContent: "center",
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
};
|
|
302
314
|
/**
|
|
303
315
|
* Button with explicit subcomponents.
|
|
304
316
|
* - `Button.Text` for label text (inherits control typography)
|
|
305
317
|
* - `Button.Icon` for icons (inherits the button's text color)
|
|
318
|
+
*
|
|
319
|
+
* The `tx`/`text` props remain the simplest path for plain labels.
|
|
306
320
|
*/
|
|
307
321
|
const Button = Object.assign(ButtonRoot, {
|
|
308
322
|
Text: ButtonText,
|