@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
|
@@ -2,60 +2,51 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { palette } from "../constants/colors.js";
|
|
3
3
|
import { spacing } from "../constants/spacing.js";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
|
-
import {
|
|
5
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
6
|
+
import { hapticSelection } from "../lib/haptics.js";
|
|
7
|
+
import { interaction } from "../constants/interaction.js";
|
|
8
|
+
import { stateSurfaceProps } from "../lib/stateSurface.js";
|
|
9
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
6
10
|
import * as SwitchPrimitives from "@rn-primitives/switch";
|
|
7
|
-
import { useCallback, useEffect, useRef
|
|
11
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
8
12
|
import { ActivityIndicator, Platform, StyleSheet, View, Animated } from "react-native";
|
|
9
13
|
import { StyledText } from "./StyledText.js";
|
|
10
14
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
11
15
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
16
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
17
|
+
import { useItemControlLabel } from "./Item.js";
|
|
12
18
|
const DEFAULT_HIT_SLOP = 8;
|
|
13
|
-
function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, height: 24 }, thumbSize = 20, loading = false, style: styleOverride, ...props }) {
|
|
19
|
+
function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, height: 24 }, thumbSize = 20, loading = false, style: styleOverride, onCheckedChange, ...props }) {
|
|
14
20
|
const { theme, getContrastingColor, getShadowStyle, getFocusRingStyle, withAlpha } = useTheme();
|
|
15
21
|
const reduceMotion = useReducedMotion();
|
|
16
22
|
const hasMounted = useRef(false);
|
|
17
|
-
const [focused, setFocused] = useState(false);
|
|
18
23
|
const focusRingStyle = getFocusRingStyle();
|
|
19
24
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
20
25
|
disabled: !!props.disabled,
|
|
21
|
-
scaleTo:
|
|
26
|
+
scaleTo: interaction.controlPressedScale,
|
|
22
27
|
haptic: false,
|
|
23
28
|
});
|
|
24
|
-
const showFocusRing = (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (target && typeof target.matches === "function") {
|
|
29
|
-
try {
|
|
30
|
-
ringVisible = target.matches(":focus-visible");
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
ringVisible = true;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
setFocused(ringVisible);
|
|
38
|
-
};
|
|
39
|
-
const hideFocusRing = () => {
|
|
40
|
-
setFocused(false);
|
|
41
|
-
};
|
|
29
|
+
const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
|
|
30
|
+
// Inside an Item row with no label of its own, the switch is named by the
|
|
31
|
+
// row title (aria-labelledby on web, accessibilityLabel on native).
|
|
32
|
+
const rowLabel = useItemControlLabel(props);
|
|
42
33
|
// Fire haptic on user-initiated toggles (skip initial mount)
|
|
43
34
|
const wrappedOnCheckedChange = useCallback((checked) => {
|
|
44
35
|
if (hasMounted.current)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}, [
|
|
36
|
+
hapticSelection();
|
|
37
|
+
onCheckedChange?.(checked);
|
|
38
|
+
}, [onCheckedChange]);
|
|
48
39
|
useEffect(() => {
|
|
49
40
|
hasMounted.current = true;
|
|
50
41
|
}, []);
|
|
51
42
|
// Single animated value drives everything: 0 = off, 1 = on
|
|
52
|
-
const progress =
|
|
43
|
+
const progress = useAnimatedValue(props.checked ? 1 : 0);
|
|
53
44
|
useEffect(() => {
|
|
54
45
|
const target = props.checked ? 1 : 0;
|
|
55
46
|
Animated.timing(progress, {
|
|
56
47
|
toValue: target,
|
|
57
48
|
duration: reduceMotion ? 0 : 120,
|
|
58
|
-
useNativeDriver:
|
|
49
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
59
50
|
}).start();
|
|
60
51
|
}, [props.checked, reduceMotion, progress]);
|
|
61
52
|
// Thumb slides from left to right with equal inset on every side.
|
|
@@ -92,7 +83,7 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
|
|
|
92
83
|
const labelFontSize = size.height / 3;
|
|
93
84
|
// Flatten style override for web compatibility
|
|
94
85
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
95
|
-
return (_jsx(Animated.View, { style: scaleStyle, children: _jsxs(SwitchPrimitives.Root, { ...props, onCheckedChange: wrappedOnCheckedChange, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
86
|
+
return (_jsx(Animated.View, { style: scaleStyle, children: _jsxs(SwitchPrimitives.Root, { ...rowLabel, ...props, onCheckedChange: wrappedOnCheckedChange, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
96
87
|
position: "relative",
|
|
97
88
|
width: size.width,
|
|
98
89
|
height: size.height,
|
|
@@ -113,7 +104,7 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
|
|
|
113
104
|
borderWidth: 1,
|
|
114
105
|
borderColor: trackBorderColor,
|
|
115
106
|
pointerEvents: "none",
|
|
116
|
-
} }), labelOn && !isIOS && (_jsx(View, { style: {
|
|
107
|
+
} }), labelOn && !isIOS && (_jsx(View, { ...stateSurfaceProps(), style: {
|
|
117
108
|
...styles.label,
|
|
118
109
|
left: labelHorizontalInset,
|
|
119
110
|
right: thumbInset + thumbSize + labelGap,
|
|
@@ -136,7 +127,7 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
|
|
|
136
127
|
...getShadowStyle("subtle"),
|
|
137
128
|
},
|
|
138
129
|
thumbAnimatedStyle,
|
|
139
|
-
], children: loading && (_jsx(ActivityIndicator, { size: "small", color: thumbIndicatorColor })) }) }), labelOff && !isIOS && (_jsx(View, { style: {
|
|
130
|
+
], children: loading && (_jsx(ActivityIndicator, { size: "small", color: thumbIndicatorColor })) }) }), labelOff && !isIOS && (_jsx(View, { ...stateSurfaceProps(), style: {
|
|
140
131
|
...styles.label,
|
|
141
132
|
left: thumbInset + thumbSize + labelGap,
|
|
142
133
|
right: labelHorizontalInset,
|
|
@@ -147,7 +138,7 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
|
|
|
147
138
|
userSelect: "none",
|
|
148
139
|
}, children: labelOff }) }))] }) }));
|
|
149
140
|
}
|
|
150
|
-
const styles = StyleSheet.create({
|
|
141
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
151
142
|
label: {
|
|
152
143
|
position: "absolute",
|
|
153
144
|
top: 0,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
3
|
import * as TabsPrimitive from "@rn-primitives/tabs";
|
|
4
|
-
import { StyledText } from "./StyledText";
|
|
5
|
-
import { type IconName } from "./Icon";
|
|
4
|
+
import { StyledText } from "./StyledText.js";
|
|
5
|
+
import { type IconName } from "./Icon.js";
|
|
6
6
|
type TabsVariant = "underline" | "pill";
|
|
7
7
|
type TabsSize = "sm" | "md";
|
|
8
8
|
export interface TabsProps extends TabsPrimitive.RootProps {
|
package/dist/components/Tabs.js
CHANGED
|
@@ -3,12 +3,15 @@ import * as React from "react";
|
|
|
3
3
|
import { Animated, Platform, StyleSheet, View } from "react-native";
|
|
4
4
|
import * as TabsPrimitive from "@rn-primitives/tabs";
|
|
5
5
|
import { StyledText } from "./StyledText.js";
|
|
6
|
-
import { TextClassContext, TextColorContext } from "./StyledText.context";
|
|
6
|
+
import { TextClassContext, TextColorContext } from "./StyledText.context.js";
|
|
7
7
|
import { Icon } from "./Icon.js";
|
|
8
8
|
import { useTheme } from "../hooks/useTheme.js";
|
|
9
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
9
10
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
10
11
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
12
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
11
13
|
import { spacing } from "../constants/spacing.js";
|
|
14
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
12
15
|
// ============================================================================
|
|
13
16
|
// Size configs
|
|
14
17
|
// ============================================================================
|
|
@@ -65,12 +68,12 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
|
|
|
65
68
|
// Determine selected state by comparing trigger value with root value
|
|
66
69
|
const rootContext = TabsPrimitive.useRootContext();
|
|
67
70
|
const isSelected = rootContext.value === value;
|
|
68
|
-
const activeOpacity =
|
|
71
|
+
const activeOpacity = useAnimatedValue(isSelected ? 1 : 0);
|
|
69
72
|
React.useEffect(() => {
|
|
70
73
|
Animated.timing(activeOpacity, {
|
|
71
74
|
toValue: isSelected ? 1 : 0,
|
|
72
75
|
duration: reduceMotion ? 0 : 200,
|
|
73
|
-
useNativeDriver:
|
|
76
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
74
77
|
}).start();
|
|
75
78
|
}, [isSelected, reduceMotion, activeOpacity]);
|
|
76
79
|
const textColor = isDisabled
|
|
@@ -79,32 +82,18 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
|
|
|
79
82
|
? theme.colors.foreground
|
|
80
83
|
: theme.colors.mutedForeground;
|
|
81
84
|
const focusRingStyle = getFocusRingStyle();
|
|
82
|
-
const [focused, setFocused] = React.useState(false);
|
|
83
85
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
84
86
|
disabled: isDisabled,
|
|
85
87
|
scaleTo: 0.97,
|
|
86
88
|
haptic: false,
|
|
87
89
|
});
|
|
88
|
-
const showFocusRing = (
|
|
89
|
-
let ringVisible = true;
|
|
90
|
-
if (Platform.OS === "web") {
|
|
91
|
-
const target = event?.nativeEvent?.target;
|
|
92
|
-
if (target && typeof target.matches === "function") {
|
|
93
|
-
try {
|
|
94
|
-
ringVisible = target.matches(":focus-visible");
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
ringVisible = true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
setFocused(ringVisible);
|
|
102
|
-
};
|
|
103
|
-
const hideFocusRing = () => {
|
|
104
|
-
setFocused(false);
|
|
105
|
-
};
|
|
90
|
+
const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
|
|
106
91
|
const triggerBaseStyle = {
|
|
107
|
-
flex: 1
|
|
92
|
+
// The wrapper below shares the list's width (`flex: 1` on the row axis);
|
|
93
|
+
// the trigger only fills it. `flex: 1` here would act on the wrapper's
|
|
94
|
+
// column axis, where a 0% basis in an indefinite-height parent collapses
|
|
95
|
+
// the trigger to its text (16px tall on web, under the 36px it declares).
|
|
96
|
+
alignSelf: "stretch",
|
|
108
97
|
// Allow the trigger to shrink below its content's intrinsic width so long
|
|
109
98
|
// labels are constrained to the trigger's flex share (and can ellipsize)
|
|
110
99
|
// instead of overflowing and clipping at the screen edge.
|
|
@@ -165,7 +154,7 @@ function TabsContent({ style, children, ...props }) {
|
|
|
165
154
|
// ============================================================================
|
|
166
155
|
// Styles
|
|
167
156
|
// ============================================================================
|
|
168
|
-
const triggerContentStyles = StyleSheet.create({
|
|
157
|
+
const triggerContentStyles = /*#__PURE__*/ StyleSheet.create({
|
|
169
158
|
container: {
|
|
170
159
|
flexDirection: "row",
|
|
171
160
|
alignItems: "center",
|
|
@@ -120,7 +120,7 @@ interface TextInputCustomProps extends TextInputProps {
|
|
|
120
120
|
* // With custom elements
|
|
121
121
|
* <TextInput
|
|
122
122
|
* label="Search"
|
|
123
|
-
* leftElement={<Icon
|
|
123
|
+
* leftElement={<Icon name="search" size={20} />}
|
|
124
124
|
* />
|
|
125
125
|
* ```
|
|
126
126
|
*/
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useCallback, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
2
|
+
import { useEffect, useCallback, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { StyleSheet, TextInput as RNTextInput, Platform, View, Pressable, } from "react-native";
|
|
4
4
|
import { Host, useNativeState, } from "@expo/ui";
|
|
5
5
|
import { NativeTextField } from "./nativeTextField";
|
|
6
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
|
+
import { shapeRadius, useShape } from "../hooks/useShape.js";
|
|
7
8
|
import { spacing } from "../constants/spacing.js";
|
|
9
|
+
import { interaction } from "../constants/interaction.js";
|
|
8
10
|
import { useFontStyle } from "../hooks/useFontStyle.js";
|
|
9
11
|
import { StyledText } from "./StyledText.js";
|
|
10
12
|
import { Icon } from "./Icon.js";
|
|
11
13
|
import { hapticLight } from "../lib/haptics.js";
|
|
12
14
|
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
15
|
+
import { stateSurfaceProps } from "../lib/stateSurface.js";
|
|
13
16
|
import { palette } from "../constants/colors.js";
|
|
14
17
|
import { clearKeyboardFocusedInput, setKeyboardFocusedInput, } from "./keyboardFocusRegistry.js";
|
|
15
18
|
import { useTextInputSurfaceResponder } from "./keyboardDismiss.js";
|
|
16
19
|
const NUMERIC_REGEX = /^[0-9]*$/;
|
|
20
|
+
/**
|
|
21
|
+
* Write the `@expo/ui` observable text buffer. `set()` is the React
|
|
22
|
+
* Compiler-friendly writer (`@expo/ui` >= 56.0.17); earlier releases in the
|
|
23
|
+
* peer range only have the `value` setter, which calls the same native
|
|
24
|
+
* `setValue`. Kept outside the component so the compiler doesn't see a hook
|
|
25
|
+
* result being mutated, which made it skip `NativeTextInput` entirely.
|
|
26
|
+
*/
|
|
27
|
+
function writeNativeText(state, text) {
|
|
28
|
+
if (typeof state.set === "function") {
|
|
29
|
+
state.set(text);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
state.value = text;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
17
35
|
/** Platform touch-target guideline (pt); the compact fields sit below it. */
|
|
18
36
|
const MIN_TOUCH_TARGET = 44;
|
|
19
37
|
const SIZE_CONFIGS = {
|
|
@@ -76,7 +94,7 @@ const SIZE_CONFIGS = {
|
|
|
76
94
|
* // With custom elements
|
|
77
95
|
* <TextInput
|
|
78
96
|
* label="Search"
|
|
79
|
-
* leftElement={<Icon
|
|
97
|
+
* leftElement={<Icon name="search" size={20} />}
|
|
80
98
|
* />
|
|
81
99
|
* ```
|
|
82
100
|
*/
|
|
@@ -96,6 +114,8 @@ export function TextInput(props) {
|
|
|
96
114
|
*/
|
|
97
115
|
function WebTextInput({ variant = "outline", size = "md", label, helperText, errorText, error, required, rows, showSecureEntryToggle, leftElement, rightElement, clearable = false, wrapperStyle, focusedStyle, forceLight, secureTextEntry, inputMode, style, onChangeText, onFocus, onBlur, value, multiline, editable = true, ref, ...rest }) {
|
|
98
116
|
const { theme, getContrastingColor, getFocusRingStyle } = useTheme();
|
|
117
|
+
// Host-app `setShape({ input })`; the underlined variant stays square.
|
|
118
|
+
const inputRadius = variant === "underlined" ? undefined : shapeRadius(useShape("input"));
|
|
99
119
|
const styles = themedStyles(theme)[variant][size];
|
|
100
120
|
const inputFont = useFontStyle("regular");
|
|
101
121
|
const [focused, setFocused] = useState(false);
|
|
@@ -152,8 +172,9 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
152
172
|
const togglePasswordVisible = () => {
|
|
153
173
|
setPasswordVisible(v => !v);
|
|
154
174
|
};
|
|
155
|
-
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [styles.wrapper, focused && getFocusRingStyle()], children: [leftElement && _jsx(View, { style: styles.leftElement, children: leftElement }), _jsx(RNTextInput, { ref: ref, ...rest, editable: editable, inputMode: inputMode || "text", multiline: multiline, numberOfLines: rows, secureTextEntry: secureTextEntry && !passwordVisible, onChangeText: inputMode === "numeric" ? handleNumericChange : handleTextChange, onFocus: showInputFocusRing, onBlur: hideInputFocusRing, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
|
|
175
|
+
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [styles.wrapper, inputRadius, focused && getFocusRingStyle()], children: [leftElement && _jsx(View, { style: styles.leftElement, children: leftElement }), _jsx(RNTextInput, { ref: ref, ...rest, editable: editable, inputMode: inputMode || "text", multiline: multiline, numberOfLines: rows, secureTextEntry: secureTextEntry && !passwordVisible, onChangeText: inputMode === "numeric" ? handleNumericChange : handleTextChange, onFocus: showInputFocusRing, onBlur: hideInputFocusRing, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
|
|
156
176
|
styles.input,
|
|
177
|
+
inputRadius,
|
|
157
178
|
// Resolved through the theme store so `setFonts` overrides apply;
|
|
158
179
|
// identical to the old hardcoded regular family by default.
|
|
159
180
|
inputFont,
|
|
@@ -180,7 +201,7 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
180
201
|
}, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) })), showClearButton && rightElement && !hasSecureToggle && (_jsxs(View, { style: styles.rightElements, children: [_jsx(Pressable, { onPress: () => {
|
|
181
202
|
hapticLight();
|
|
182
203
|
onChangeText?.("");
|
|
183
|
-
}, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) }), rightElement] })), !showClearButton && rightElement && !hasSecureToggle && (_jsx(View, { style: styles.rightElement, children: rightElement })), secureTextEntry && showSecureEntryToggle && (_jsx(Pressable, { style: styles.passwordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) })), showErrorIcon && (_jsx(View, { style: [styles.errorIcon, { pointerEvents: "none" }], accessibilityLabel: "Error", children: _jsx(Icon, { name: "circle-alert", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [
|
|
204
|
+
}, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) }), rightElement] })), !showClearButton && rightElement && !hasSecureToggle && (_jsx(View, { style: styles.rightElement, children: rightElement })), secureTextEntry && showSecureEntryToggle && (_jsx(Pressable, { style: styles.passwordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim", decorative: true }) })), showErrorIcon && (_jsx(View, { style: [styles.errorIcon, { pointerEvents: "none" }], accessibilityLabel: "Error", children: _jsx(Icon, { name: "circle-alert", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [
|
|
184
205
|
styles.helperText,
|
|
185
206
|
hasError && styles.errorText,
|
|
186
207
|
], children: errorText || helperText }))] }));
|
|
@@ -220,6 +241,7 @@ function NativeTextInput({ variant = "outline", size = "md", label, helperText,
|
|
|
220
241
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
221
242
|
leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
222
243
|
const { theme, getContrastingColor } = useTheme();
|
|
244
|
+
const inputShape = useShape("input");
|
|
223
245
|
const styles = themedStyles(theme)[variant][size];
|
|
224
246
|
const inputFont = useFontStyle("regular");
|
|
225
247
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
@@ -240,8 +262,13 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
240
262
|
const [handoff, setHandoff] = useState(null);
|
|
241
263
|
const handoffRef = useRef(null);
|
|
242
264
|
const isFocusedRef = useRef(false);
|
|
265
|
+
// Latest flavour for the focus/blur/toggle handlers and the handoff timer,
|
|
266
|
+
// synced after each commit (before any native event can be dispatched)
|
|
267
|
+
// instead of written during render.
|
|
243
268
|
const activeSecureRef = useRef(effectiveSecureTextEntry);
|
|
244
|
-
|
|
269
|
+
useLayoutEffect(() => {
|
|
270
|
+
activeSecureRef.current = effectiveSecureTextEntry;
|
|
271
|
+
});
|
|
245
272
|
// Taps that land while a handoff is in flight are counted and applied (by
|
|
246
273
|
// parity) once it settles.
|
|
247
274
|
const queuedTogglesRef = useRef(0);
|
|
@@ -249,7 +276,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
249
276
|
// iOS-only per-flavour mount generation. Bumped whenever a flavour becomes
|
|
250
277
|
// the incoming view so React mounts a FRESH native view (whose `autoFocus`
|
|
251
278
|
// will run) instead of reusing one still unmounting from the previous handoff.
|
|
252
|
-
|
|
279
|
+
// State, not a ref: it keys the rendered Host, and the bump lands in the same
|
|
280
|
+
// batch as the `passwordVisible` flip that makes that flavour incoming.
|
|
281
|
+
const [generation, setGeneration] = useState({ secure: 0, plain: 0 });
|
|
253
282
|
// Last text the JS side knows about; see handleChangeText.
|
|
254
283
|
const lastTextRef = useRef(value ?? defaultValue ?? "");
|
|
255
284
|
// Armed when the hide toggle hands focus to the SecureField (iOS); see
|
|
@@ -262,7 +291,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
262
291
|
// programmatic sets); typing already updated `state` natively.
|
|
263
292
|
useEffect(() => {
|
|
264
293
|
if (value !== undefined && value !== state.value) {
|
|
265
|
-
state
|
|
294
|
+
writeNativeText(state, value);
|
|
266
295
|
}
|
|
267
296
|
if (value !== undefined)
|
|
268
297
|
lastTextRef.current = value;
|
|
@@ -347,10 +376,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
347
376
|
restoreAfterSecureHandoffRef.current = false;
|
|
348
377
|
const outgoingSecure = activeSecureRef.current;
|
|
349
378
|
if (Platform.OS === "ios") {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
generationRef.current.secure += 1;
|
|
379
|
+
setGeneration((current) => outgoingSecure
|
|
380
|
+
? { ...current, plain: current.plain + 1 }
|
|
381
|
+
: { ...current, secure: current.secure + 1 });
|
|
354
382
|
}
|
|
355
383
|
if (Platform.OS === "ios" && isFocusedRef.current) {
|
|
356
384
|
const timer = setTimeout(() => {
|
|
@@ -376,7 +404,11 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
376
404
|
}
|
|
377
405
|
setPasswordVisible((v) => !v);
|
|
378
406
|
}, [activeInput, finishHandoff, focusRegistryToken, parentOnBlur]);
|
|
379
|
-
|
|
407
|
+
// `finishHandoff` replays queued taps through this ref (it is declared
|
|
408
|
+
// before the toggle it calls). Synced after commit, like `activeSecureRef`.
|
|
409
|
+
useLayoutEffect(() => {
|
|
410
|
+
toggleRef.current = togglePasswordVisible;
|
|
411
|
+
}, [togglePasswordVisible]);
|
|
380
412
|
// iOS clears a secure field's existing text on the first keystroke after it
|
|
381
413
|
// becomes first responder (text it did not see typed). Right after the hide
|
|
382
414
|
// toggle hands focus to the SecureField that reads as "type one character,
|
|
@@ -395,7 +427,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
395
427
|
const next = wipedByKey ? previous + text : wipedByBackspace ? previous.slice(0, -1) : text;
|
|
396
428
|
lastTextRef.current = next;
|
|
397
429
|
if (wiped)
|
|
398
|
-
state
|
|
430
|
+
writeNativeText(state, next);
|
|
399
431
|
onChangeText?.(next);
|
|
400
432
|
}, [onChangeText, state]);
|
|
401
433
|
useEffect(() => {
|
|
@@ -418,13 +450,13 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
418
450
|
focus: () => activeInput()?.focus(),
|
|
419
451
|
blur: () => blurAll(),
|
|
420
452
|
clear: () => {
|
|
421
|
-
state
|
|
453
|
+
writeNativeText(state, "");
|
|
422
454
|
lastTextRef.current = "";
|
|
423
455
|
},
|
|
424
456
|
isFocused: () => activeInput()?.isFocused() ?? false,
|
|
425
457
|
setNativeProps: (props) => {
|
|
426
458
|
if (typeof props?.text === "string") {
|
|
427
|
-
state
|
|
459
|
+
writeNativeText(state, props.text);
|
|
428
460
|
lastTextRef.current = props.text;
|
|
429
461
|
}
|
|
430
462
|
},
|
|
@@ -451,9 +483,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
451
483
|
const surfaceStyle = {
|
|
452
484
|
backgroundColor,
|
|
453
485
|
borderColor,
|
|
454
|
-
borderRadius: variant === "underlined" ? 0 : spacing.radiusMd,
|
|
486
|
+
borderRadius: variant === "underlined" ? 0 : inputShape?.borderRadius ?? spacing.radiusMd,
|
|
455
487
|
borderWidth: variant === "outline" ? 1 : 0,
|
|
456
|
-
opacity: editable === false ?
|
|
488
|
+
opacity: editable === false ? interaction.disabledOpacity : 1,
|
|
457
489
|
overflow: "hidden",
|
|
458
490
|
};
|
|
459
491
|
// Native field: transparent, padding only. The visible surface is drawn by
|
|
@@ -487,7 +519,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
487
519
|
fontSize: sizeConfig.fontSize,
|
|
488
520
|
...(nativeFontFamily ? { fontFamily: nativeFontFamily } : null),
|
|
489
521
|
};
|
|
490
|
-
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [surfaceStyle, hasSecureToggle && styles.nativeRow], hitSlop: { top: surfaceHitSlop, bottom: surfaceHitSlop }, ...surfaceResponderProps, children: [(Platform.OS === "android" ? [effectiveSecureTextEntry] : [true, false]).map((secure) => {
|
|
522
|
+
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { ...stateSurfaceProps(), style: [surfaceStyle, hasSecureToggle && styles.nativeRow], hitSlop: { top: surfaceHitSlop, bottom: surfaceHitSlop }, ...surfaceResponderProps, children: [(Platform.OS === "android" ? [effectiveSecureTextEntry] : [true, false]).map((secure) => {
|
|
491
523
|
const active = secure === effectiveSecureTextEntry;
|
|
492
524
|
const outgoing = handoff != null && secure === handoff.outgoingSecure;
|
|
493
525
|
if (!active && !outgoing)
|
|
@@ -502,9 +534,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
502
534
|
: styles.nativeHostHandoff, pointerEvents: inLayout ? "auto" : "none", accessibilityElementsHidden: !inLayout, importantForAccessibility: inLayout ? "auto" : "no-hide-descendants", children: _jsx(NativeTextField, { ...rest, autoFocus: rest.autoFocus || (handoff != null && active), ref: Platform.OS === "android" ? androidRef : secure ? secureRef : plainRef, value: state, defaultValue: defaultValue, onChangeText: handleChangeText, onFocus: () => handleFocus(secure), onBlur: () => handleBlur(secure), editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: secure, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) }, Platform.OS === "android"
|
|
503
535
|
? "android"
|
|
504
536
|
: secure
|
|
505
|
-
? `secure-${
|
|
506
|
-
: `plain-${
|
|
507
|
-
}), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
|
|
537
|
+
? `secure-${generation.secure}`
|
|
538
|
+
: `plain-${generation.plain}`));
|
|
539
|
+
}), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
|
|
508
540
|
}
|
|
509
541
|
const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
510
542
|
nativeHost: {
|
|
@@ -529,6 +561,10 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
529
561
|
},
|
|
530
562
|
nativePasswordToggle: {
|
|
531
563
|
paddingHorizontal: spacing.xs,
|
|
564
|
+
minWidth: spacing.minTarget,
|
|
565
|
+
minHeight: spacing.minTarget,
|
|
566
|
+
alignItems: "center",
|
|
567
|
+
justifyContent: "center",
|
|
532
568
|
},
|
|
533
569
|
wrapper: {
|
|
534
570
|
width: "100%",
|
|
@@ -554,7 +590,7 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
554
590
|
borderBottomWidth: 2,
|
|
555
591
|
},
|
|
556
592
|
disabled: {
|
|
557
|
-
opacity:
|
|
593
|
+
opacity: interaction.disabledOpacity,
|
|
558
594
|
...(Platform.OS === "web" && { cursor: "not-allowed" }),
|
|
559
595
|
},
|
|
560
596
|
error: {
|
|
@@ -596,11 +632,17 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
596
632
|
transform: [{ translateY: -10 }],
|
|
597
633
|
zIndex: 1,
|
|
598
634
|
},
|
|
635
|
+
// The eye/clear buttons draw a 16-20px glyph; the pressable itself is at
|
|
636
|
+
// least `spacing.minTarget` square (WCAG 2.5.8 — `hitSlop` is inert on web).
|
|
599
637
|
passwordToggle: {
|
|
600
638
|
position: "absolute",
|
|
601
|
-
right: spacing.sm,
|
|
639
|
+
right: spacing.sm - (spacing.minTarget - 20) / 2,
|
|
602
640
|
top: "50%",
|
|
603
|
-
|
|
641
|
+
minWidth: spacing.minTarget,
|
|
642
|
+
minHeight: spacing.minTarget,
|
|
643
|
+
alignItems: "center",
|
|
644
|
+
justifyContent: "center",
|
|
645
|
+
transform: [{ translateY: -spacing.minTarget / 2 }],
|
|
604
646
|
zIndex: 1,
|
|
605
647
|
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
606
648
|
},
|
|
@@ -613,9 +655,13 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
613
655
|
},
|
|
614
656
|
clearButton: {
|
|
615
657
|
position: "absolute",
|
|
616
|
-
right: spacing.sm,
|
|
658
|
+
right: spacing.sm - (spacing.minTarget - 20) / 2,
|
|
617
659
|
top: "50%",
|
|
618
|
-
|
|
660
|
+
minWidth: spacing.minTarget,
|
|
661
|
+
minHeight: spacing.minTarget,
|
|
662
|
+
alignItems: "center",
|
|
663
|
+
justifyContent: "center",
|
|
664
|
+
transform: [{ translateY: -spacing.minTarget / 2 }],
|
|
619
665
|
zIndex: 1,
|
|
620
666
|
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
621
667
|
},
|
|
@@ -632,7 +678,7 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
632
678
|
});
|
|
633
679
|
const VARIANT_KEYS = ["outline", "filled", "underlined"];
|
|
634
680
|
const SIZE_KEYS = ["sm", "md", "lg"];
|
|
635
|
-
const themedStyles = createThemedStyles((theme) => Object.fromEntries(VARIANT_KEYS.map((variant) => [
|
|
681
|
+
const themedStyles = /*#__PURE__*/ createThemedStyles((theme) => Object.fromEntries(VARIANT_KEYS.map((variant) => [
|
|
636
682
|
variant,
|
|
637
683
|
Object.fromEntries(SIZE_KEYS.map((size) => [size, createStyles(theme, variant, size)])),
|
|
638
684
|
])));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import * as TogglePrimitive from "@rn-primitives/toggle";
|
|
3
3
|
import { ViewStyle, StyleProp } from "react-native";
|
|
4
|
-
import type { IconName } from "./Icon";
|
|
4
|
+
import type { IconName } from "./Icon.js";
|
|
5
5
|
export type ToggleVariant = "default" | "outline";
|
|
6
6
|
export type ToggleSize = "sm" | "default" | "lg";
|
|
7
7
|
export type ToggleShape = "default" | "square" | "circle";
|
|
@@ -44,47 +44,7 @@ interface ToggleProps extends Omit<TogglePrimitive.RootProps, "style"> {
|
|
|
44
44
|
*/
|
|
45
45
|
style?: StyleProp<ViewStyle>;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
* Enhanced Toggle Component
|
|
49
|
-
*
|
|
50
|
-
* Features:
|
|
51
|
-
* - 2 variants (default, outline)
|
|
52
|
-
* - 3 sizes (sm, default, lg)
|
|
53
|
-
* - 3 shapes (default, square, circle)
|
|
54
|
-
* - Loading state with spinner
|
|
55
|
-
* - Icon-only optimization
|
|
56
|
-
* - Style prop support
|
|
57
|
-
* - Full accessibility
|
|
58
|
-
*
|
|
59
|
-
* Usage:
|
|
60
|
-
* ```tsx
|
|
61
|
-
* // Basic
|
|
62
|
-
* const [pressed, setPressed] = useState(false);
|
|
63
|
-
* <Toggle pressed={pressed} onPressedChange={setPressed}>
|
|
64
|
-
* <SansSerifText>Toggle Me</SansSerifText>
|
|
65
|
-
* </Toggle>
|
|
66
|
-
*
|
|
67
|
-
* // With icon
|
|
68
|
-
* <Toggle pressed={pressed} onPressedChange={setPressed} iconOnly>
|
|
69
|
-
* <ToggleIcon name="bold" />
|
|
70
|
-
* </Toggle>
|
|
71
|
-
*
|
|
72
|
-
* // Different shapes
|
|
73
|
-
* <Toggle shape="square" pressed={pressed} onPressedChange={setPressed}>
|
|
74
|
-
* <SansSerifText>Square</SansSerifText>
|
|
75
|
-
* </Toggle>
|
|
76
|
-
*
|
|
77
|
-
* <Toggle shape="circle" iconOnly pressed={pressed} onPressedChange={setPressed}>
|
|
78
|
-
* <ToggleIcon name="bold" />
|
|
79
|
-
* </Toggle>
|
|
80
|
-
*
|
|
81
|
-
* // Loading state
|
|
82
|
-
* <Toggle loading pressed={pressed} onPressedChange={setPressed}>
|
|
83
|
-
* <SansSerifText>Loading...</SansSerifText>
|
|
84
|
-
* </Toggle>
|
|
85
|
-
* ```
|
|
86
|
-
*/
|
|
87
|
-
declare function Toggle({ variant, size, shape, loading, iconOnly, style: styleOverride, ...props }: ToggleProps): React.JSX.Element;
|
|
47
|
+
declare function Toggle({ variant, size, shape, loading, iconOnly, style: styleOverride, onPressedChange, ...props }: ToggleProps): React.JSX.Element;
|
|
88
48
|
/**
|
|
89
49
|
* ToggleIcon Component
|
|
90
50
|
* Icon wrapper for use inside Toggle buttons
|