@mrmeg/expo-ui 0.27.1 → 0.29.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 +666 -0
- package/LLM_USAGE.md +241 -37
- package/README.md +591 -136
- package/bin/expo-ui-icons.mjs +509 -0
- 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 +71 -28
- package/dist/components/Icon.js +70 -28
- package/dist/components/IconGlyph.d.ts +15 -0
- package/dist/components/IconGlyph.js +32 -0
- package/dist/components/IconGlyph.native.d.ts +10 -0
- package/dist/components/IconGlyph.native.js +30 -0
- 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/ThemeSelector.d.ts +52 -0
- package/dist/components/ThemeSelector.js +90 -0
- package/dist/components/Toggle.d.ts +5 -49
- package/dist/components/Toggle.js +53 -29
- package/dist/components/ToggleGroup.d.ts +4 -8
- package/dist/components/ToggleGroup.js +52 -34
- 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/iconBuiltins.generated.d.ts +143 -0
- package/dist/components/iconBuiltins.generated.js +25 -0
- package/dist/components/iconTypes.d.ts +37 -0
- package/dist/components/iconTypes.js +1 -0
- package/dist/components/index.d.ts +47 -44
- package/dist/components/index.js +3 -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 +429 -132
- 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/icons/LICENSE.lucide +43 -0
- package/dist/icons/lucide.json +2144 -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 +59 -10
- package/llms.txt +10 -1
- package/package.json +79 -4
- package/dist/components/iconRegistry.generated.d.ts +0 -148
- package/dist/components/iconRegistry.generated.js +0 -296
|
@@ -1,10 +1,12 @@
|
|
|
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";
|
|
@@ -15,6 +17,21 @@ import { palette } from "../constants/colors.js";
|
|
|
15
17
|
import { clearKeyboardFocusedInput, setKeyboardFocusedInput, } from "./keyboardFocusRegistry.js";
|
|
16
18
|
import { useTextInputSurfaceResponder } from "./keyboardDismiss.js";
|
|
17
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
|
+
}
|
|
18
35
|
/** Platform touch-target guideline (pt); the compact fields sit below it. */
|
|
19
36
|
const MIN_TOUCH_TARGET = 44;
|
|
20
37
|
const SIZE_CONFIGS = {
|
|
@@ -77,7 +94,7 @@ const SIZE_CONFIGS = {
|
|
|
77
94
|
* // With custom elements
|
|
78
95
|
* <TextInput
|
|
79
96
|
* label="Search"
|
|
80
|
-
* leftElement={<Icon
|
|
97
|
+
* leftElement={<Icon name="search" size={20} />}
|
|
81
98
|
* />
|
|
82
99
|
* ```
|
|
83
100
|
*/
|
|
@@ -97,6 +114,8 @@ export function TextInput(props) {
|
|
|
97
114
|
*/
|
|
98
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 }) {
|
|
99
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"));
|
|
100
119
|
const styles = themedStyles(theme)[variant][size];
|
|
101
120
|
const inputFont = useFontStyle("regular");
|
|
102
121
|
const [focused, setFocused] = useState(false);
|
|
@@ -153,8 +172,9 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
153
172
|
const togglePasswordVisible = () => {
|
|
154
173
|
setPasswordVisible(v => !v);
|
|
155
174
|
};
|
|
156
|
-
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: [
|
|
157
176
|
styles.input,
|
|
177
|
+
inputRadius,
|
|
158
178
|
// Resolved through the theme store so `setFonts` overrides apply;
|
|
159
179
|
// identical to the old hardcoded regular family by default.
|
|
160
180
|
inputFont,
|
|
@@ -181,7 +201,7 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
181
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: () => {
|
|
182
202
|
hapticLight();
|
|
183
203
|
onChangeText?.("");
|
|
184
|
-
}, 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: [
|
|
185
205
|
styles.helperText,
|
|
186
206
|
hasError && styles.errorText,
|
|
187
207
|
], children: errorText || helperText }))] }));
|
|
@@ -221,6 +241,7 @@ function NativeTextInput({ variant = "outline", size = "md", label, helperText,
|
|
|
221
241
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
222
242
|
leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
223
243
|
const { theme, getContrastingColor } = useTheme();
|
|
244
|
+
const inputShape = useShape("input");
|
|
224
245
|
const styles = themedStyles(theme)[variant][size];
|
|
225
246
|
const inputFont = useFontStyle("regular");
|
|
226
247
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
@@ -241,8 +262,13 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
241
262
|
const [handoff, setHandoff] = useState(null);
|
|
242
263
|
const handoffRef = useRef(null);
|
|
243
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.
|
|
244
268
|
const activeSecureRef = useRef(effectiveSecureTextEntry);
|
|
245
|
-
|
|
269
|
+
useLayoutEffect(() => {
|
|
270
|
+
activeSecureRef.current = effectiveSecureTextEntry;
|
|
271
|
+
});
|
|
246
272
|
// Taps that land while a handoff is in flight are counted and applied (by
|
|
247
273
|
// parity) once it settles.
|
|
248
274
|
const queuedTogglesRef = useRef(0);
|
|
@@ -250,7 +276,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
250
276
|
// iOS-only per-flavour mount generation. Bumped whenever a flavour becomes
|
|
251
277
|
// the incoming view so React mounts a FRESH native view (whose `autoFocus`
|
|
252
278
|
// will run) instead of reusing one still unmounting from the previous handoff.
|
|
253
|
-
|
|
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 });
|
|
254
282
|
// Last text the JS side knows about; see handleChangeText.
|
|
255
283
|
const lastTextRef = useRef(value ?? defaultValue ?? "");
|
|
256
284
|
// Armed when the hide toggle hands focus to the SecureField (iOS); see
|
|
@@ -263,7 +291,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
263
291
|
// programmatic sets); typing already updated `state` natively.
|
|
264
292
|
useEffect(() => {
|
|
265
293
|
if (value !== undefined && value !== state.value) {
|
|
266
|
-
state
|
|
294
|
+
writeNativeText(state, value);
|
|
267
295
|
}
|
|
268
296
|
if (value !== undefined)
|
|
269
297
|
lastTextRef.current = value;
|
|
@@ -348,10 +376,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
348
376
|
restoreAfterSecureHandoffRef.current = false;
|
|
349
377
|
const outgoingSecure = activeSecureRef.current;
|
|
350
378
|
if (Platform.OS === "ios") {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
generationRef.current.secure += 1;
|
|
379
|
+
setGeneration((current) => outgoingSecure
|
|
380
|
+
? { ...current, plain: current.plain + 1 }
|
|
381
|
+
: { ...current, secure: current.secure + 1 });
|
|
355
382
|
}
|
|
356
383
|
if (Platform.OS === "ios" && isFocusedRef.current) {
|
|
357
384
|
const timer = setTimeout(() => {
|
|
@@ -377,7 +404,11 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
377
404
|
}
|
|
378
405
|
setPasswordVisible((v) => !v);
|
|
379
406
|
}, [activeInput, finishHandoff, focusRegistryToken, parentOnBlur]);
|
|
380
|
-
|
|
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]);
|
|
381
412
|
// iOS clears a secure field's existing text on the first keystroke after it
|
|
382
413
|
// becomes first responder (text it did not see typed). Right after the hide
|
|
383
414
|
// toggle hands focus to the SecureField that reads as "type one character,
|
|
@@ -396,7 +427,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
396
427
|
const next = wipedByKey ? previous + text : wipedByBackspace ? previous.slice(0, -1) : text;
|
|
397
428
|
lastTextRef.current = next;
|
|
398
429
|
if (wiped)
|
|
399
|
-
state
|
|
430
|
+
writeNativeText(state, next);
|
|
400
431
|
onChangeText?.(next);
|
|
401
432
|
}, [onChangeText, state]);
|
|
402
433
|
useEffect(() => {
|
|
@@ -419,13 +450,13 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
419
450
|
focus: () => activeInput()?.focus(),
|
|
420
451
|
blur: () => blurAll(),
|
|
421
452
|
clear: () => {
|
|
422
|
-
state
|
|
453
|
+
writeNativeText(state, "");
|
|
423
454
|
lastTextRef.current = "";
|
|
424
455
|
},
|
|
425
456
|
isFocused: () => activeInput()?.isFocused() ?? false,
|
|
426
457
|
setNativeProps: (props) => {
|
|
427
458
|
if (typeof props?.text === "string") {
|
|
428
|
-
state
|
|
459
|
+
writeNativeText(state, props.text);
|
|
429
460
|
lastTextRef.current = props.text;
|
|
430
461
|
}
|
|
431
462
|
},
|
|
@@ -452,9 +483,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
452
483
|
const surfaceStyle = {
|
|
453
484
|
backgroundColor,
|
|
454
485
|
borderColor,
|
|
455
|
-
borderRadius: variant === "underlined" ? 0 : spacing.radiusMd,
|
|
486
|
+
borderRadius: variant === "underlined" ? 0 : inputShape?.borderRadius ?? spacing.radiusMd,
|
|
456
487
|
borderWidth: variant === "outline" ? 1 : 0,
|
|
457
|
-
opacity: editable === false ?
|
|
488
|
+
opacity: editable === false ? interaction.disabledOpacity : 1,
|
|
458
489
|
overflow: "hidden",
|
|
459
490
|
};
|
|
460
491
|
// Native field: transparent, padding only. The visible surface is drawn by
|
|
@@ -503,9 +534,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
503
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"
|
|
504
535
|
? "android"
|
|
505
536
|
: secure
|
|
506
|
-
? `secure-${
|
|
507
|
-
: `plain-${
|
|
508
|
-
}), 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 }))] }));
|
|
509
540
|
}
|
|
510
541
|
const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
511
542
|
nativeHost: {
|
|
@@ -530,6 +561,10 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
530
561
|
},
|
|
531
562
|
nativePasswordToggle: {
|
|
532
563
|
paddingHorizontal: spacing.xs,
|
|
564
|
+
minWidth: spacing.minTarget,
|
|
565
|
+
minHeight: spacing.minTarget,
|
|
566
|
+
alignItems: "center",
|
|
567
|
+
justifyContent: "center",
|
|
533
568
|
},
|
|
534
569
|
wrapper: {
|
|
535
570
|
width: "100%",
|
|
@@ -555,7 +590,7 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
555
590
|
borderBottomWidth: 2,
|
|
556
591
|
},
|
|
557
592
|
disabled: {
|
|
558
|
-
opacity:
|
|
593
|
+
opacity: interaction.disabledOpacity,
|
|
559
594
|
...(Platform.OS === "web" && { cursor: "not-allowed" }),
|
|
560
595
|
},
|
|
561
596
|
error: {
|
|
@@ -597,11 +632,17 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
597
632
|
transform: [{ translateY: -10 }],
|
|
598
633
|
zIndex: 1,
|
|
599
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).
|
|
600
637
|
passwordToggle: {
|
|
601
638
|
position: "absolute",
|
|
602
|
-
right: spacing.sm,
|
|
639
|
+
right: spacing.sm - (spacing.minTarget - 20) / 2,
|
|
603
640
|
top: "50%",
|
|
604
|
-
|
|
641
|
+
minWidth: spacing.minTarget,
|
|
642
|
+
minHeight: spacing.minTarget,
|
|
643
|
+
alignItems: "center",
|
|
644
|
+
justifyContent: "center",
|
|
645
|
+
transform: [{ translateY: -spacing.minTarget / 2 }],
|
|
605
646
|
zIndex: 1,
|
|
606
647
|
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
607
648
|
},
|
|
@@ -614,9 +655,13 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
614
655
|
},
|
|
615
656
|
clearButton: {
|
|
616
657
|
position: "absolute",
|
|
617
|
-
right: spacing.sm,
|
|
658
|
+
right: spacing.sm - (spacing.minTarget - 20) / 2,
|
|
618
659
|
top: "50%",
|
|
619
|
-
|
|
660
|
+
minWidth: spacing.minTarget,
|
|
661
|
+
minHeight: spacing.minTarget,
|
|
662
|
+
alignItems: "center",
|
|
663
|
+
justifyContent: "center",
|
|
664
|
+
transform: [{ translateY: -spacing.minTarget / 2 }],
|
|
620
665
|
zIndex: 1,
|
|
621
666
|
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
622
667
|
},
|
|
@@ -633,7 +678,7 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
633
678
|
});
|
|
634
679
|
const VARIANT_KEYS = ["outline", "filled", "underlined"];
|
|
635
680
|
const SIZE_KEYS = ["sm", "md", "lg"];
|
|
636
|
-
const themedStyles = createThemedStyles((theme) => Object.fromEntries(VARIANT_KEYS.map((variant) => [
|
|
681
|
+
const themedStyles = /*#__PURE__*/ createThemedStyles((theme) => Object.fromEntries(VARIANT_KEYS.map((variant) => [
|
|
637
682
|
variant,
|
|
638
683
|
Object.fromEntries(SIZE_KEYS.map((size) => [size, createStyles(theme, variant, size)])),
|
|
639
684
|
])));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { type ThemePreference } from "../state/themeStore.js";
|
|
4
|
+
/**
|
|
5
|
+
* ThemeSelector — a compact System / Light / Dark switch wired to the kit's
|
|
6
|
+
* theme store.
|
|
7
|
+
*
|
|
8
|
+
* Icon-only by default (about 116pt wide), sized to sit in an `Item` row's
|
|
9
|
+
* `ItemActions` so the whole appearance setting is one row instead of three.
|
|
10
|
+
* Pass `showLabels` for a full-width control with text under a group title.
|
|
11
|
+
*
|
|
12
|
+
* Reads and writes `useThemeStore` unless `value` / `onValueChange` are
|
|
13
|
+
* passed. The package has no i18n: pass translated `labels`, which become the
|
|
14
|
+
* visible text (with `showLabels`) and each segment's accessibility label.
|
|
15
|
+
*
|
|
16
|
+
* Drawn in RN on every platform (no native segmented control), so the icons
|
|
17
|
+
* render on iOS and Android too and the server render matches the client: the
|
|
18
|
+
* store boots `system` on both sides until the persisted preference loads.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <Item>
|
|
23
|
+
* <ItemContent>
|
|
24
|
+
* <ItemTitle>Theme</ItemTitle>
|
|
25
|
+
* </ItemContent>
|
|
26
|
+
* <ItemActions>
|
|
27
|
+
* <ThemeSelector />
|
|
28
|
+
* </ItemActions>
|
|
29
|
+
* </Item>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export type ThemeSelectorLabels = Record<ThemePreference, string>;
|
|
33
|
+
export interface ThemeSelectorProps {
|
|
34
|
+
/** Controlled preference. Defaults to the theme store's `userTheme`. */
|
|
35
|
+
value?: ThemePreference;
|
|
36
|
+
/** Called with the chosen preference. Defaults to the theme store's `setTheme`. */
|
|
37
|
+
onValueChange?: (value: ThemePreference) => void;
|
|
38
|
+
/** Segment labels, for screen readers and `showLabels`. @default English */
|
|
39
|
+
labels?: Partial<ThemeSelectorLabels>;
|
|
40
|
+
/** Show the label beside each icon and stretch to the parent's width. @default false */
|
|
41
|
+
showLabels?: boolean;
|
|
42
|
+
/** Disable interaction. @default false */
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
/** Names the group for screen readers. Inside an `Item`, defaults to the row title. */
|
|
45
|
+
accessibilityLabel?: string;
|
|
46
|
+
/** Style override for the track. */
|
|
47
|
+
style?: StyleProp<ViewStyle>;
|
|
48
|
+
/** Test id for the track; segments get `${testID}-${value}`. @default "theme-selector" */
|
|
49
|
+
testID?: string;
|
|
50
|
+
}
|
|
51
|
+
declare function ThemeSelector({ value, onValueChange, labels, showLabels, disabled, accessibilityLabel, style, testID, }: ThemeSelectorProps): React.JSX.Element;
|
|
52
|
+
export { ThemeSelector };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Platform, Pressable, StyleSheet, View } from "react-native";
|
|
3
|
+
import { useTheme } from "../hooks/useTheme.js";
|
|
4
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
5
|
+
import { useThemeStore } from "../state/themeStore.js";
|
|
6
|
+
import { hapticSelection } from "../lib/haptics.js";
|
|
7
|
+
import { spacing } from "../constants/spacing.js";
|
|
8
|
+
import { interaction } from "../constants/interaction.js";
|
|
9
|
+
import { Icon } from "./Icon.js";
|
|
10
|
+
import { StyledText } from "./StyledText.js";
|
|
11
|
+
import { useItemControlLabel } from "./Item.js";
|
|
12
|
+
const DEFAULT_LABELS = { system: "System", light: "Light", dark: "Dark" };
|
|
13
|
+
const OPTIONS = [
|
|
14
|
+
// A phone reads as "device" on native; web visitors are mostly on a desktop.
|
|
15
|
+
{ value: "system", icon: Platform.OS === "web" ? "monitor" : "smartphone" },
|
|
16
|
+
{ value: "light", icon: "sun" },
|
|
17
|
+
{ value: "dark", icon: "moon" },
|
|
18
|
+
];
|
|
19
|
+
const TRACK_HEIGHT = 32;
|
|
20
|
+
const TRACK_INSET = 2;
|
|
21
|
+
const ICON_SEGMENT_WIDTH = 36;
|
|
22
|
+
function ThemeSelector({ value, onValueChange, labels, showLabels = false, disabled = false, accessibilityLabel, style, testID = "theme-selector", }) {
|
|
23
|
+
const rowLabel = useItemControlLabel({ accessibilityLabel });
|
|
24
|
+
const { theme } = useTheme();
|
|
25
|
+
const userTheme = useThemeStore((state) => state.userTheme);
|
|
26
|
+
const setTheme = useThemeStore((state) => state.setTheme);
|
|
27
|
+
const selected = value ?? userTheme;
|
|
28
|
+
const text = { ...DEFAULT_LABELS, ...labels };
|
|
29
|
+
const select = (next) => {
|
|
30
|
+
if (next === selected)
|
|
31
|
+
return;
|
|
32
|
+
hapticSelection();
|
|
33
|
+
(onValueChange ?? setTheme)(next);
|
|
34
|
+
};
|
|
35
|
+
return (_jsx(View, { testID: testID, accessibilityRole: "radiogroup", accessibilityLabel: accessibilityLabel, ...rowLabel, accessibilityState: { disabled }, style: [
|
|
36
|
+
styles.track,
|
|
37
|
+
{ backgroundColor: theme.colors.muted, opacity: disabled ? interaction.disabledOpacity : 1 },
|
|
38
|
+
showLabels ? styles.trackStretch : styles.trackCompact,
|
|
39
|
+
style,
|
|
40
|
+
], children: OPTIONS.map((option) => (_jsx(Segment, { testID: `${testID}-${option.value}`, icon: option.icon, label: text[option.value], showLabel: showLabels, selected: option.value === selected, disabled: disabled, onPress: () => select(option.value) }, option.value))) }));
|
|
41
|
+
}
|
|
42
|
+
function Segment({ testID, icon, label, showLabel, selected, disabled, onPress }) {
|
|
43
|
+
const { theme, getFocusRingStyle } = useTheme();
|
|
44
|
+
const focus = useFocusVisible();
|
|
45
|
+
const color = selected ? theme.colors.foreground : theme.colors.mutedForeground;
|
|
46
|
+
return (_jsxs(Pressable, { testID: testID, accessibilityRole: "radio", accessibilityLabel: label, accessibilityState: { checked: selected, disabled }, disabled: disabled, onPress: onPress, onFocus: focus.onFocus, onBlur: focus.onBlur,
|
|
47
|
+
// The track is 32pt; reach the 44pt touch target vertically.
|
|
48
|
+
hitSlop: { top: 6, bottom: 6 }, style: ({ pressed }) => [
|
|
49
|
+
styles.segment,
|
|
50
|
+
showLabel ? styles.segmentStretch : styles.segmentCompact,
|
|
51
|
+
selected && { backgroundColor: theme.colors.background, borderColor: theme.colors.border },
|
|
52
|
+
pressed && !selected && { opacity: interaction.pressedOpacity },
|
|
53
|
+
Platform.OS === "web" && {
|
|
54
|
+
cursor: (disabled ? "not-allowed" : "pointer"),
|
|
55
|
+
outlineStyle: "none",
|
|
56
|
+
},
|
|
57
|
+
focus.focused && !disabled && getFocusRingStyle(),
|
|
58
|
+
], children: [_jsx(Icon, { name: icon, size: 16, color: color, decorative: true }), showLabel && (_jsx(StyledText, { selectable: false, numberOfLines: 1, fontWeight: "medium", style: { fontSize: 13, lineHeight: 18, color }, children: label }))] }));
|
|
59
|
+
}
|
|
60
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
61
|
+
track: {
|
|
62
|
+
flexDirection: "row",
|
|
63
|
+
height: TRACK_HEIGHT,
|
|
64
|
+
padding: TRACK_INSET,
|
|
65
|
+
borderRadius: spacing.radiusMd,
|
|
66
|
+
},
|
|
67
|
+
trackCompact: {
|
|
68
|
+
alignSelf: "flex-start",
|
|
69
|
+
},
|
|
70
|
+
trackStretch: {
|
|
71
|
+
alignSelf: "stretch",
|
|
72
|
+
},
|
|
73
|
+
segment: {
|
|
74
|
+
flexDirection: "row",
|
|
75
|
+
alignItems: "center",
|
|
76
|
+
justifyContent: "center",
|
|
77
|
+
gap: spacing.xs,
|
|
78
|
+
borderRadius: spacing.radiusSm,
|
|
79
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
80
|
+
borderColor: "transparent",
|
|
81
|
+
},
|
|
82
|
+
segmentCompact: {
|
|
83
|
+
width: ICON_SEGMENT_WIDTH,
|
|
84
|
+
},
|
|
85
|
+
segmentStretch: {
|
|
86
|
+
flex: 1,
|
|
87
|
+
paddingHorizontal: spacing.sm,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
export { ThemeSelector };
|
|
@@ -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 {
|
|
4
|
+
import type { IconProps } 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,51 +44,12 @@ 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
|
|
91
|
-
*
|
|
51
|
+
* Takes `Icon`'s props; the color defaults to the Toggle's text color, and the
|
|
52
|
+
* icon is decorative (the control's label names it).
|
|
92
53
|
*
|
|
93
54
|
* Usage:
|
|
94
55
|
* ```tsx
|
|
@@ -97,11 +58,6 @@ declare function Toggle({ variant, size, shape, loading, iconOnly, style: styleO
|
|
|
97
58
|
* </Toggle>
|
|
98
59
|
* ```
|
|
99
60
|
*/
|
|
100
|
-
|
|
101
|
-
name: IconName;
|
|
102
|
-
size?: number;
|
|
103
|
-
color?: string;
|
|
104
|
-
}
|
|
105
|
-
declare function ToggleIcon({ name, size, color }: ToggleIconProps): React.JSX.Element;
|
|
61
|
+
declare function ToggleIcon(props: IconProps): React.JSX.Element;
|
|
106
62
|
export { Toggle, ToggleIcon };
|
|
107
63
|
export type { ToggleProps };
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
3
|
import { Icon } from "./Icon.js";
|
|
4
|
-
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
|
|
4
|
+
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context.js";
|
|
5
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
6
7
|
import { spacing } from "../constants/spacing.js";
|
|
8
|
+
import { getWebHitInsets, WebHitTarget } from "../lib/webHitTarget.js";
|
|
9
|
+
import { interaction } from "../constants/interaction.js";
|
|
10
|
+
import { hapticSelection } from "../lib/haptics.js";
|
|
7
11
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
12
|
+
import { useItemControlLabel } from "./Item.js";
|
|
8
13
|
import * as TogglePrimitive from "@rn-primitives/toggle";
|
|
9
14
|
import { Platform, StyleSheet, ActivityIndicator, Animated } from "react-native";
|
|
10
15
|
import { palette } from "../constants/colors.js";
|
|
@@ -73,11 +78,12 @@ const TOGGLE_SIZES = {
|
|
|
73
78
|
* </Toggle>
|
|
74
79
|
* ```
|
|
75
80
|
*/
|
|
76
|
-
|
|
81
|
+
// Dev-only, once per session: the same unnamed toggle re-renders constantly.
|
|
82
|
+
let warnedIconOnly = false;
|
|
83
|
+
function Toggle({ variant = "default", size = "default", shape = "default", loading = false, iconOnly = false, style: styleOverride, onPressedChange, ...props }) {
|
|
77
84
|
const { theme, getContrastingColor, getFocusRingStyle, withAlpha } = useTheme();
|
|
78
85
|
const sizeConfig = TOGGLE_SIZES[size];
|
|
79
86
|
const focusRingStyle = getFocusRingStyle();
|
|
80
|
-
const [focused, setFocused] = useState(false);
|
|
81
87
|
// Calculate text color based on state and variant
|
|
82
88
|
const getTextColor = () => {
|
|
83
89
|
if (props.pressed) {
|
|
@@ -106,28 +112,33 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
|
|
|
106
112
|
const children = props.children;
|
|
107
113
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
108
114
|
disabled: !!isDisabled,
|
|
109
|
-
scaleTo:
|
|
115
|
+
scaleTo: interaction.controlPressedScale,
|
|
110
116
|
haptic: false,
|
|
111
117
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
ringVisible = target.matches(":focus-visible");
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
ringVisible = true;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
setFocused(ringVisible);
|
|
126
|
-
};
|
|
127
|
-
const hideFocusRing = () => {
|
|
128
|
-
setFocused(false);
|
|
118
|
+
// A toggle is a selection control: its state change is the haptic moment,
|
|
119
|
+
// not the press, so it follows the `"selection"` setting like Switch does.
|
|
120
|
+
const handlePressedChange = (next) => {
|
|
121
|
+
hapticSelection();
|
|
122
|
+
onPressedChange?.(next);
|
|
129
123
|
};
|
|
130
|
-
|
|
124
|
+
const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
|
|
125
|
+
const rowLabel = useItemControlLabel(props);
|
|
126
|
+
if (process.env.NODE_ENV !== "production" &&
|
|
127
|
+
iconOnly &&
|
|
128
|
+
!warnedIconOnly &&
|
|
129
|
+
props.accessibilityLabel === undefined &&
|
|
130
|
+
props["aria-label"] === undefined &&
|
|
131
|
+
props["aria-labelledby"] === undefined &&
|
|
132
|
+
rowLabel.accessibilityLabel === undefined &&
|
|
133
|
+
rowLabel["aria-labelledby"] === undefined) {
|
|
134
|
+
warnedIconOnly = true;
|
|
135
|
+
console.warn("Toggle: an icon-only toggle (`iconOnly`) has no accessible name. Pass `accessibilityLabel` " +
|
|
136
|
+
"(what the toggle turns on, e.g. \"Bold\") so screen readers announce more than \"button\".");
|
|
137
|
+
}
|
|
138
|
+
// Web pointer target: 44pt tall through a transparent child inside the drawn
|
|
139
|
+
// box (react-native-web ignores `hitSlop`); native keeps DEFAULT_HIT_SLOP.
|
|
140
|
+
const hitTarget = (_jsx(WebHitTarget, { insets: Platform.OS === "web" ? getWebHitInsets(undefined, sizeConfig.height) : null, border: styles.root.borderWidth }));
|
|
141
|
+
return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(TogglePrimitive.Root, { ...rowLabel, ...props, disabled: isDisabled, onPressedChange: handlePressedChange, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
131
142
|
...styles.root,
|
|
132
143
|
height: sizeConfig.height,
|
|
133
144
|
minWidth: sizeConfig.minWidth,
|
|
@@ -152,7 +163,7 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
|
|
|
152
163
|
borderColor: theme.colors.primary,
|
|
153
164
|
}),
|
|
154
165
|
// Disabled state
|
|
155
|
-
opacity: isDisabled ?
|
|
166
|
+
opacity: isDisabled ? interaction.disabledOpacity : 1,
|
|
156
167
|
// Web-specific styles
|
|
157
168
|
...(Platform.OS === "web" && {
|
|
158
169
|
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
@@ -166,13 +177,26 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
|
|
|
166
177
|
selected: props.pressed,
|
|
167
178
|
disabled: !!isDisabled,
|
|
168
179
|
busy: loading,
|
|
169
|
-
}, children: loading ? (_jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(ActivityIndicator, { size: "small", color: textColor }) })) : typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }) }));
|
|
180
|
+
}, children: loading ? (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(ActivityIndicator, { size: "small", color: textColor }) }), hitTarget] })) : typeof children === "function" ? ((state) => (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }), hitTarget] }))) : (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children }), hitTarget] })) }) }) }) }));
|
|
170
181
|
}
|
|
171
|
-
|
|
182
|
+
/**
|
|
183
|
+
* ToggleIcon Component
|
|
184
|
+
* Icon wrapper for use inside Toggle buttons
|
|
185
|
+
* Takes `Icon`'s props; the color defaults to the Toggle's text color, and the
|
|
186
|
+
* icon is decorative (the control's label names it).
|
|
187
|
+
*
|
|
188
|
+
* Usage:
|
|
189
|
+
* ```tsx
|
|
190
|
+
* <Toggle pressed={pressed} onPressedChange={setPressed}>
|
|
191
|
+
* <ToggleIcon name="bold" />
|
|
192
|
+
* </Toggle>
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
function ToggleIcon(props) {
|
|
172
196
|
const contextColor = React.use(TextColorContext);
|
|
173
|
-
return _jsx(Icon, {
|
|
197
|
+
return _jsx(Icon, { decorative: true, color: contextColor, ...props });
|
|
174
198
|
}
|
|
175
|
-
const styles = StyleSheet.create({
|
|
199
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
176
200
|
root: {
|
|
177
201
|
flexDirection: "row",
|
|
178
202
|
alignItems: "center",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IconProps } from "./Icon.js";
|
|
2
2
|
import * as ToggleGroupPrimitive from "@rn-primitives/toggle-group";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
type ToggleGroupVariant = "default" | "outline";
|
|
@@ -62,7 +62,8 @@ declare function ToggleGroupItem({ isFirst, isLast, children, ...props }: Toggle
|
|
|
62
62
|
/**
|
|
63
63
|
* ToggleGroupIcon Component
|
|
64
64
|
* Icon wrapper for use inside ToggleGroup items
|
|
65
|
-
*
|
|
65
|
+
* Takes `Icon`'s props; the color defaults to the ToggleGroup's text color, and the
|
|
66
|
+
* icon is decorative (the control's label names it).
|
|
66
67
|
*
|
|
67
68
|
* Usage:
|
|
68
69
|
* ```tsx
|
|
@@ -71,11 +72,6 @@ declare function ToggleGroupItem({ isFirst, isLast, children, ...props }: Toggle
|
|
|
71
72
|
* </ToggleGroupItem>
|
|
72
73
|
* ```
|
|
73
74
|
*/
|
|
74
|
-
|
|
75
|
-
name: IconName;
|
|
76
|
-
size?: number;
|
|
77
|
-
color?: string;
|
|
78
|
-
}
|
|
79
|
-
declare function ToggleGroupIcon({ name, size, color }: ToggleGroupIconProps): React.JSX.Element;
|
|
75
|
+
declare function ToggleGroupIcon(props: IconProps): React.JSX.Element;
|
|
80
76
|
export { ToggleGroup, ToggleGroupIcon, ToggleGroupItem };
|
|
81
77
|
export type { ToggleGroupProps, ToggleGroupSize, ToggleGroupVariant };
|