@yahoo/uds-mobile 2.21.2 → 2.22.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/README.md +2 -0
- package/dist/components/IconButton.cjs +24 -21
- package/dist/components/IconButton.js +25 -22
- package/dist/components/IconButton.js.map +1 -1
- package/dist/components/Switch.cjs +34 -12
- package/dist/components/Switch.d.cts.map +1 -1
- package/dist/components/Switch.d.ts.map +1 -1
- package/dist/components/Switch.js +36 -14
- package/dist/components/Switch.js.map +1 -1
- package/dist/components/UDSGestureProvider.cjs +4 -0
- package/dist/components/UDSGestureProvider.d.cts +3 -0
- package/dist/components/UDSGestureProvider.d.ts +3 -0
- package/dist/components/UDSGestureProvider.js +3 -0
- package/dist/components/UDSProvider.cjs +10 -5
- package/dist/components/UDSProvider.d.cts +15 -7
- package/dist/components/UDSProvider.d.cts.map +1 -1
- package/dist/components/UDSProvider.d.ts +15 -7
- package/dist/components/UDSProvider.d.ts.map +1 -1
- package/dist/components/UDSProvider.js +10 -6
- package/dist/components/UDSProvider.js.map +1 -1
- package/dist/components/internal/Overlay/OverlayPortal.js.map +1 -1
- package/dist/jest/mocks/styles.cjs +21 -9
- package/dist/jest/mocks/styles.d.cts.map +1 -1
- package/dist/jest/mocks/styles.d.ts.map +1 -1
- package/dist/jest/mocks/styles.js +21 -9
- package/dist/jest/mocks/styles.js.map +1 -1
- package/dist/jest/mocks/unistyles.cjs +16 -1
- package/dist/jest/mocks/unistyles.d.cts +56 -2
- package/dist/jest/mocks/unistyles.d.cts.map +1 -1
- package/dist/jest/mocks/unistyles.d.ts +56 -2
- package/dist/jest/mocks/unistyles.d.ts.map +1 -1
- package/dist/jest/mocks/unistyles.js +16 -1
- package/dist/jest/mocks/unistyles.js.map +1 -1
- package/dist/portal.cjs +1 -1
- package/dist/portal.js +1 -1
- package/dist/portal.js.map +1 -1
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -681,6 +681,7 @@ import type { HStackProps } from '@yahoo/uds-mobile/HStack';
|
|
|
681
681
|
import type { ScreenProps } from '@yahoo/uds-mobile/Screen';
|
|
682
682
|
import type { PressableProps } from '@yahoo/uds-mobile/Pressable';
|
|
683
683
|
import type { IconSlotProps, IconSlotType } from '@yahoo/uds-mobile/IconSlot';
|
|
684
|
+
import type { UDSGestureProviderProps } from '@yahoo/uds-mobile/UDSGestureProvider';
|
|
684
685
|
```
|
|
685
686
|
|
|
686
687
|
### Exports
|
|
@@ -708,6 +709,7 @@ import { RadioGroup } from '@yahoo/uds-mobile/RadioGroup';
|
|
|
708
709
|
import { Screen } from '@yahoo/uds-mobile/Screen';
|
|
709
710
|
import { Switch } from '@yahoo/uds-mobile/Switch';
|
|
710
711
|
import { Text } from '@yahoo/uds-mobile/Text';
|
|
712
|
+
import { UDSGestureProvider } from '@yahoo/uds-mobile/UDSGestureProvider';
|
|
711
713
|
import { VStack } from '@yahoo/uds-mobile/VStack';
|
|
712
714
|
|
|
713
715
|
// Sub-exports
|
|
@@ -59,6 +59,7 @@ function interpolateShadowAlpha(shadow, alpha) {
|
|
|
59
59
|
const IconButton = (0, react.memo)(function IconButton({ name, variant = "primary", size = "md", iconVariant = "outline", iconColor, loading, disabled, style, accessibilityLabel, accessibilityHint, disableEffects = false, onPressIn, onPressOut, ref, ...props }) {
|
|
60
60
|
const isDisabled = disabled || loading;
|
|
61
61
|
const shouldAnimate = !disableEffects && !isDisabled;
|
|
62
|
+
const shouldAnimateVariantColors = react_native.Platform.OS !== "web";
|
|
62
63
|
const { theme } = (0, react_native_unistyles.useUnistyles)();
|
|
63
64
|
const { controlHeight } = (0, react.useMemo)(() => require_components_Button_buttonTheme.getIconButtonControlMetrics(theme, size), [theme, size]);
|
|
64
65
|
const matchedControlDimensions = controlHeight > 0 ? {
|
|
@@ -66,16 +67,16 @@ const IconButton = (0, react.memo)(function IconButton({ name, variant = "primar
|
|
|
66
67
|
width: controlHeight
|
|
67
68
|
} : void 0;
|
|
68
69
|
const [pressed, setPressed] = (0, react.useState)(false);
|
|
69
|
-
generated_styles.iconButtonStyles.useVariants({ size });
|
|
70
|
-
generated_styles.buttonStyles.useVariants({
|
|
70
|
+
const resolvedIconButtonStyles = generated_styles.iconButtonStyles.useVariants({ size }) ?? generated_styles.iconButtonStyles;
|
|
71
|
+
const resolvedButtonStyles = generated_styles.buttonStyles.useVariants({
|
|
71
72
|
variant,
|
|
72
73
|
disabled: isDisabled,
|
|
73
74
|
pressed
|
|
74
|
-
});
|
|
75
|
-
generated_styles.styles.useVariants({ color: iconColor });
|
|
76
|
-
const resolvedIconColor = iconColor ?
|
|
77
|
-
const backgroundColor = (0, react_native_unistyles_reanimated.useAnimatedVariantColor)(
|
|
78
|
-
const borderColor = (0, react_native_unistyles_reanimated.useAnimatedVariantColor)(
|
|
75
|
+
}) ?? generated_styles.buttonStyles;
|
|
76
|
+
const resolvedFoundationStyles = generated_styles.styles.useVariants({ color: iconColor }) ?? generated_styles.styles;
|
|
77
|
+
const resolvedIconColor = iconColor ? resolvedFoundationStyles.foundation.color : void 0;
|
|
78
|
+
const backgroundColor = (0, react_native_unistyles_reanimated.useAnimatedVariantColor)(resolvedButtonStyles.root, "backgroundColor");
|
|
79
|
+
const borderColor = (0, react_native_unistyles_reanimated.useAnimatedVariantColor)(resolvedButtonStyles.root, "borderColor");
|
|
79
80
|
const animatedTheme = (0, react_native_unistyles_reanimated.useAnimatedTheme)();
|
|
80
81
|
const scale = (0, react_native_reanimated.useSharedValue)(require_index.SCALE_EFFECTS.none);
|
|
81
82
|
const handlePressIn = (0, react.useCallback)((event) => {
|
|
@@ -108,14 +109,16 @@ const IconButton = (0, react.memo)(function IconButton({ name, variant = "primar
|
|
|
108
109
|
const shadowPressed = animatedTheme.value.components[`button/variant/${variant}/root/pressed`]?.boxShadow;
|
|
109
110
|
return {
|
|
110
111
|
transform: [{ scale: scale.value }],
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
...shouldAnimateVariantColors && {
|
|
113
|
+
backgroundColor: (0, react_native_reanimated.withTiming)(backgroundColor.value, {
|
|
114
|
+
duration: 220,
|
|
115
|
+
easing: react_native_reanimated.Easing.bezier(0, 0, .2, 1)
|
|
116
|
+
}),
|
|
117
|
+
borderColor: (0, react_native_reanimated.withTiming)(borderColor.value, {
|
|
118
|
+
duration: 220,
|
|
119
|
+
easing: react_native_reanimated.Easing.bezier(0, 0, .2, 1)
|
|
120
|
+
})
|
|
121
|
+
},
|
|
119
122
|
...shadowPressed && { boxShadow: interpolateShadowAlpha(shadowPressed, pressProgress.value) }
|
|
120
123
|
};
|
|
121
124
|
});
|
|
@@ -133,21 +136,21 @@ const IconButton = (0, react.memo)(function IconButton({ name, variant = "primar
|
|
|
133
136
|
accessibilityRole: "button",
|
|
134
137
|
accessibilityState: a11yState,
|
|
135
138
|
style: [
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
resolvedIconButtonStyles.root,
|
|
140
|
+
resolvedButtonStyles.root,
|
|
138
141
|
matchedControlDimensions,
|
|
139
|
-
|
|
142
|
+
resolvedFoundationStyles.foundation,
|
|
140
143
|
animatedRootStyle,
|
|
141
144
|
typeof style === "function" ? style({ pressed }) : style
|
|
142
145
|
],
|
|
143
146
|
...props,
|
|
144
147
|
children: loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.ActivityIndicator, {
|
|
145
|
-
size:
|
|
146
|
-
color: resolvedIconColor ??
|
|
148
|
+
size: resolvedIconButtonStyles.icon.fontSize,
|
|
149
|
+
color: resolvedIconColor ?? resolvedButtonStyles.icon.color
|
|
147
150
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_Icon.Icon, {
|
|
148
151
|
name,
|
|
149
152
|
variant: iconVariant,
|
|
150
|
-
style: [
|
|
153
|
+
style: [resolvedIconButtonStyles.icon, resolvedButtonStyles.icon],
|
|
151
154
|
dangerouslySetColor: resolvedIconColor
|
|
152
155
|
})
|
|
153
156
|
});
|
|
@@ -5,7 +5,7 @@ import { Icon } from "./Icon.js";
|
|
|
5
5
|
import { getIconButtonControlMetrics } from "./Button/buttonTheme.js";
|
|
6
6
|
import { AnimatedPressable } from "./Pressable.js";
|
|
7
7
|
import { memo, useCallback, useMemo, useState } from "react";
|
|
8
|
-
import { ActivityIndicator } from "react-native";
|
|
8
|
+
import { ActivityIndicator, Platform } from "react-native";
|
|
9
9
|
import { jsx } from "react/jsx-runtime";
|
|
10
10
|
import { buttonStyles, iconButtonStyles, styles } from "../../generated/styles";
|
|
11
11
|
import { useUnistyles } from "react-native-unistyles";
|
|
@@ -57,6 +57,7 @@ function interpolateShadowAlpha(shadow, alpha) {
|
|
|
57
57
|
const IconButton = memo(function IconButton({ name, variant = "primary", size = "md", iconVariant = "outline", iconColor, loading, disabled, style, accessibilityLabel, accessibilityHint, disableEffects = false, onPressIn, onPressOut, ref, ...props }) {
|
|
58
58
|
const isDisabled = disabled || loading;
|
|
59
59
|
const shouldAnimate = !disableEffects && !isDisabled;
|
|
60
|
+
const shouldAnimateVariantColors = Platform.OS !== "web";
|
|
60
61
|
const { theme } = useUnistyles();
|
|
61
62
|
const { controlHeight } = useMemo(() => getIconButtonControlMetrics(theme, size), [theme, size]);
|
|
62
63
|
const matchedControlDimensions = controlHeight > 0 ? {
|
|
@@ -64,16 +65,16 @@ const IconButton = memo(function IconButton({ name, variant = "primary", size =
|
|
|
64
65
|
width: controlHeight
|
|
65
66
|
} : void 0;
|
|
66
67
|
const [pressed, setPressed] = useState(false);
|
|
67
|
-
iconButtonStyles.useVariants({ size });
|
|
68
|
-
buttonStyles.useVariants({
|
|
68
|
+
const resolvedIconButtonStyles = iconButtonStyles.useVariants({ size }) ?? iconButtonStyles;
|
|
69
|
+
const resolvedButtonStyles = buttonStyles.useVariants({
|
|
69
70
|
variant,
|
|
70
71
|
disabled: isDisabled,
|
|
71
72
|
pressed
|
|
72
|
-
});
|
|
73
|
-
styles.useVariants({ color: iconColor });
|
|
74
|
-
const resolvedIconColor = iconColor ?
|
|
75
|
-
const backgroundColor = useAnimatedVariantColor(
|
|
76
|
-
const borderColor = useAnimatedVariantColor(
|
|
73
|
+
}) ?? buttonStyles;
|
|
74
|
+
const resolvedFoundationStyles = styles.useVariants({ color: iconColor }) ?? styles;
|
|
75
|
+
const resolvedIconColor = iconColor ? resolvedFoundationStyles.foundation.color : void 0;
|
|
76
|
+
const backgroundColor = useAnimatedVariantColor(resolvedButtonStyles.root, "backgroundColor");
|
|
77
|
+
const borderColor = useAnimatedVariantColor(resolvedButtonStyles.root, "borderColor");
|
|
77
78
|
const animatedTheme = useAnimatedTheme();
|
|
78
79
|
const scale = useSharedValue(SCALE_EFFECTS.none);
|
|
79
80
|
const handlePressIn = useCallback((event) => {
|
|
@@ -106,14 +107,16 @@ const IconButton = memo(function IconButton({ name, variant = "primary", size =
|
|
|
106
107
|
const shadowPressed = animatedTheme.value.components[`button/variant/${variant}/root/pressed`]?.boxShadow;
|
|
107
108
|
return {
|
|
108
109
|
transform: [{ scale: scale.value }],
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
...shouldAnimateVariantColors && {
|
|
111
|
+
backgroundColor: withTiming(backgroundColor.value, {
|
|
112
|
+
duration: 220,
|
|
113
|
+
easing: Easing.bezier(0, 0, .2, 1)
|
|
114
|
+
}),
|
|
115
|
+
borderColor: withTiming(borderColor.value, {
|
|
116
|
+
duration: 220,
|
|
117
|
+
easing: Easing.bezier(0, 0, .2, 1)
|
|
118
|
+
})
|
|
119
|
+
},
|
|
117
120
|
...shadowPressed && { boxShadow: interpolateShadowAlpha(shadowPressed, pressProgress.value) }
|
|
118
121
|
};
|
|
119
122
|
});
|
|
@@ -131,21 +134,21 @@ const IconButton = memo(function IconButton({ name, variant = "primary", size =
|
|
|
131
134
|
accessibilityRole: "button",
|
|
132
135
|
accessibilityState: a11yState,
|
|
133
136
|
style: [
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
resolvedIconButtonStyles.root,
|
|
138
|
+
resolvedButtonStyles.root,
|
|
136
139
|
matchedControlDimensions,
|
|
137
|
-
|
|
140
|
+
resolvedFoundationStyles.foundation,
|
|
138
141
|
animatedRootStyle,
|
|
139
142
|
typeof style === "function" ? style({ pressed }) : style
|
|
140
143
|
],
|
|
141
144
|
...props,
|
|
142
145
|
children: loading ? /* @__PURE__ */ jsx(ActivityIndicator, {
|
|
143
|
-
size:
|
|
144
|
-
color: resolvedIconColor ??
|
|
146
|
+
size: resolvedIconButtonStyles.icon.fontSize,
|
|
147
|
+
color: resolvedIconColor ?? resolvedButtonStyles.icon.color
|
|
145
148
|
}) : /* @__PURE__ */ jsx(Icon, {
|
|
146
149
|
name,
|
|
147
150
|
variant: iconVariant,
|
|
148
|
-
style: [
|
|
151
|
+
style: [resolvedIconButtonStyles.icon, resolvedButtonStyles.icon],
|
|
149
152
|
dangerouslySetColor: resolvedIconColor
|
|
150
153
|
})
|
|
151
154
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IconButton.js","names":["foundationStyles"],"sources":["../../src/components/IconButton.tsx"],"sourcesContent":["import type { ButtonVariantFlat, IconButtonSize, IconVariant } from '@yahoo/uds-types';\nimport type { Ref } from 'react';\nimport { memo, useCallback, useMemo, useState } from 'react';\nimport type { View } from 'react-native';\nimport { ActivityIndicator } from 'react-native';\nimport {\n Easing,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n withSpring,\n withTiming,\n} from 'react-native-reanimated';\n// eslint-disable-next-line uds/no-use-unistyles -- iconbutton control height from theme size layers\nimport { useUnistyles } from 'react-native-unistyles';\nimport { useAnimatedTheme, useAnimatedVariantColor } from 'react-native-unistyles/reanimated';\n\nimport type { StyleProps } from '../../generated/styles';\nimport { buttonStyles, iconButtonStyles, styles as foundationStyles } from '../../generated/styles';\nimport { BUTTON_SPRING_CONFIG, SCALE_EFFECTS } from '../motion';\nimport { getIconButtonControlMetrics } from './Button/buttonTheme';\nimport type { IconName } from './Icon';\nimport { Icon } from './Icon';\nimport type { PressableProps } from './Pressable';\nimport { AnimatedPressable } from './Pressable';\n\n/* -------------------------------------------------------------------------- */\n/* Animation Helpers */\n/* -------------------------------------------------------------------------- */\n\nfunction interpolateShadowAlpha(shadow: string | undefined, alpha: number): string {\n 'worklet';\n if (!shadow) {\n return '';\n }\n if (alpha >= 1) {\n return shadow;\n }\n if (alpha <= 0) {\n return '';\n }\n\n return shadow.replace(/rgba\\(([^,]+),\\s*([^,]+),\\s*([^,]+),\\s*([^)]+)\\)/g, (_, r, g, b, a) => {\n const newAlpha = parseFloat(a) * alpha;\n return `rgba(${r}, ${g}, ${b}, ${newAlpha.toFixed(3)})`;\n });\n}\n\n/* -------------------------------------------------------------------------- */\n/* IconButton Props */\n/* -------------------------------------------------------------------------- */\n\ninterface IconButtonProps extends Omit<PressableProps, 'children'> {\n /** Icon to render from the icons package */\n name: IconName;\n /** The visual style variant @default 'primary' */\n variant?: ButtonVariantFlat;\n /** The size of the button @default 'md' */\n size?: IconButtonSize;\n /** The icon style variant @default 'outline' */\n iconVariant?: IconVariant;\n /** Override the icon color token without changing the button variant tokens */\n iconColor?: StyleProps['color'];\n /** Shows a loading spinner and disables the button */\n loading?: boolean;\n /**\n * Disable motion effects (scale on press, icon animations)\n * @default false\n */\n disableEffects?: boolean;\n /** Ref to the underlying View */\n ref?: Ref<View>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* IconButton Component */\n/* -------------------------------------------------------------------------- */\n\n/**\n * **An icon button element that can be used to trigger an action**\n *\n * @description\n * An icon-only button for actions where space is limited. Features animated\n * scale effect on press and smooth color transitions matching the web UDS\n * IconButton behavior.\n *\n * @category Interactive\n * @platform mobile\n *\n * @example\n * ```tsx\n * import { IconButton } from '@yahoo/uds-mobile/IconButton';\n *\n * <IconButton name=\"Add\" onPress={() => console.log('pressed')} />\n * <IconButton name=\"Close\" variant=\"secondary\" size=\"sm\" />\n * <IconButton name=\"Settings\" loading />\n * ```\n *\n * @usage\n * - Use for toolbar actions\n * - Use for closing modals/dialogs\n * - Always provide accessibilityLabel for screen readers\n *\n * @accessibility\n * - Sets `accessibilityRole=\"button\"` automatically\n * - Announces loading state to screen readers\n * - **Always** provide `accessibilityLabel` since there's no visible text\n *\n * @see {@link Button} for buttons with text labels\n * @see {@link Icon} for non-interactive icons\n */\nconst IconButton = memo(function IconButton({\n name,\n variant = 'primary',\n size = 'md',\n iconVariant = 'outline',\n iconColor,\n loading,\n disabled,\n style,\n accessibilityLabel,\n accessibilityHint,\n disableEffects = false,\n onPressIn,\n onPressOut,\n ref,\n ...props\n}: IconButtonProps) {\n const isDisabled = disabled || loading;\n const shouldAnimate = !disableEffects && !isDisabled;\n\n const { theme } = useUnistyles();\n const { controlHeight } = useMemo(() => getIconButtonControlMetrics(theme, size), [theme, size]);\n const matchedControlDimensions =\n controlHeight > 0 ? ({ height: controlHeight, width: controlHeight } as const) : undefined;\n\n /* --------------------------------- State ---------------------------------- */\n const [pressed, setPressed] = useState(false);\n\n // Apply layer-based styles with compound variant support\n iconButtonStyles.useVariants({ size });\n buttonStyles.useVariants({ variant, disabled: isDisabled, pressed });\n foundationStyles.useVariants({ color: iconColor });\n\n const resolvedIconColor = iconColor\n ? (foundationStyles.foundation.color as string | undefined)\n : undefined;\n\n // Animate colors using Unistyles' useAnimatedVariantColor\n const backgroundColor = useAnimatedVariantColor(buttonStyles.root, 'backgroundColor');\n const borderColor = useAnimatedVariantColor(buttonStyles.root, 'borderColor');\n\n // Get animated theme for boxShadow\n const animatedTheme = useAnimatedTheme();\n\n /* ------------------------------- Animation -------------------------------- */\n const scale = useSharedValue<number>(SCALE_EFFECTS.none);\n\n const handlePressIn = useCallback<NonNullable<PressableProps['onPressIn']>>(\n (event) => {\n setPressed(true);\n if (shouldAnimate) {\n scale.value = withSpring(SCALE_EFFECTS.down, BUTTON_SPRING_CONFIG);\n }\n onPressIn?.(event);\n },\n [shouldAnimate, scale, onPressIn],\n );\n\n const handlePressOut = useCallback<NonNullable<PressableProps['onPressOut']>>(\n (event) => {\n setPressed(false);\n if (shouldAnimate) {\n scale.value = withSpring(SCALE_EFFECTS.none, BUTTON_SPRING_CONFIG);\n }\n onPressOut?.(event);\n },\n [shouldAnimate, scale, onPressOut],\n );\n\n const a11yState = useMemo(() => ({ disabled: isDisabled, busy: loading }), [isDisabled, loading]);\n\n /* --------------------------------- Styles --------------------------------- */\n // Animate pressed state for shadow\n const pressProgress = useDerivedValue(\n () => withTiming(pressed ? 1 : 0, { duration: 220, easing: Easing.bezier(0, 0, 0.2, 1) }),\n [pressed],\n );\n\n // Animate using Unistyles' variant color system + boxShadow from theme\n const animatedRootStyle = useAnimatedStyle(() => {\n // Get boxShadow from theme using flattened path (no camelCase conversion needed!)\n const components = animatedTheme.value.components as unknown as Record<\n string,\n Record<string, unknown>\n >;\n const shadowPressed = components[`button/variant/${variant}/root/pressed`]?.boxShadow as\n | string\n | undefined;\n\n return {\n transform: [{ scale: scale.value }],\n backgroundColor: withTiming(backgroundColor.value, {\n duration: 220,\n easing: Easing.bezier(0, 0, 0.2, 1),\n }),\n borderColor: withTiming(borderColor.value, {\n duration: 220,\n easing: Easing.bezier(0, 0, 0.2, 1),\n }),\n // Only animate shadow if the theme defines one for this variant\n ...(shadowPressed && {\n boxShadow: interpolateShadowAlpha(shadowPressed, pressProgress.value),\n }),\n };\n });\n\n /* --------------------------------- Render --------------------------------- */\n return (\n <AnimatedPressable\n ref={ref}\n disabled={isDisabled}\n onPressIn={handlePressIn}\n onPressOut={handlePressOut}\n flexDirection=\"row\"\n alignItems=\"center\"\n justifyContent=\"center\"\n overflow=\"hidden\"\n accessibilityLabel={loading ? `${accessibilityLabel ?? ''}, loading` : accessibilityLabel}\n accessibilityHint={accessibilityHint}\n accessibilityRole=\"button\"\n accessibilityState={a11yState}\n style={[\n iconButtonStyles.root,\n buttonStyles.root,\n matchedControlDimensions,\n foundationStyles.foundation,\n animatedRootStyle,\n typeof style === 'function' ? style({ pressed }) : style,\n ]}\n {...props}\n >\n {loading ? (\n <ActivityIndicator\n size={iconButtonStyles.icon.fontSize}\n color={resolvedIconColor ?? buttonStyles.icon.color}\n />\n ) : (\n <Icon\n name={name}\n variant={iconVariant}\n style={[iconButtonStyles.icon, buttonStyles.icon]}\n dangerouslySetColor={resolvedIconColor}\n />\n )}\n </AnimatedPressable>\n );\n});\n\nIconButton.displayName = 'IconButton';\n\nexport { IconButton, type IconButtonProps };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"IconButton.js","names":["foundationStyles"],"sources":["../../src/components/IconButton.tsx"],"sourcesContent":["import type { ButtonVariantFlat, IconButtonSize, IconVariant } from '@yahoo/uds-types';\nimport type { Ref } from 'react';\nimport { memo, useCallback, useMemo, useState } from 'react';\nimport type { View } from 'react-native';\nimport { ActivityIndicator, Platform } from 'react-native';\nimport {\n Easing,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n withSpring,\n withTiming,\n} from 'react-native-reanimated';\n// eslint-disable-next-line uds/no-use-unistyles -- iconbutton control height from theme size layers\nimport { useUnistyles } from 'react-native-unistyles';\nimport { useAnimatedTheme, useAnimatedVariantColor } from 'react-native-unistyles/reanimated';\n\nimport type { StyleProps } from '../../generated/styles';\nimport { buttonStyles, iconButtonStyles, styles as foundationStyles } from '../../generated/styles';\nimport { BUTTON_SPRING_CONFIG, SCALE_EFFECTS } from '../motion';\nimport { getIconButtonControlMetrics } from './Button/buttonTheme';\nimport type { IconName } from './Icon';\nimport { Icon } from './Icon';\nimport type { PressableProps } from './Pressable';\nimport { AnimatedPressable } from './Pressable';\n\n/* -------------------------------------------------------------------------- */\n/* Animation Helpers */\n/* -------------------------------------------------------------------------- */\n\nfunction interpolateShadowAlpha(shadow: string | undefined, alpha: number): string {\n 'worklet';\n if (!shadow) {\n return '';\n }\n if (alpha >= 1) {\n return shadow;\n }\n if (alpha <= 0) {\n return '';\n }\n\n return shadow.replace(/rgba\\(([^,]+),\\s*([^,]+),\\s*([^,]+),\\s*([^)]+)\\)/g, (_, r, g, b, a) => {\n const newAlpha = parseFloat(a) * alpha;\n return `rgba(${r}, ${g}, ${b}, ${newAlpha.toFixed(3)})`;\n });\n}\n\n/* -------------------------------------------------------------------------- */\n/* IconButton Props */\n/* -------------------------------------------------------------------------- */\n\ninterface IconButtonProps extends Omit<PressableProps, 'children'> {\n /** Icon to render from the icons package */\n name: IconName;\n /** The visual style variant @default 'primary' */\n variant?: ButtonVariantFlat;\n /** The size of the button @default 'md' */\n size?: IconButtonSize;\n /** The icon style variant @default 'outline' */\n iconVariant?: IconVariant;\n /** Override the icon color token without changing the button variant tokens */\n iconColor?: StyleProps['color'];\n /** Shows a loading spinner and disables the button */\n loading?: boolean;\n /**\n * Disable motion effects (scale on press, icon animations)\n * @default false\n */\n disableEffects?: boolean;\n /** Ref to the underlying View */\n ref?: Ref<View>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* IconButton Component */\n/* -------------------------------------------------------------------------- */\n\n/**\n * **An icon button element that can be used to trigger an action**\n *\n * @description\n * An icon-only button for actions where space is limited. Features animated\n * scale effect on press and smooth color transitions matching the web UDS\n * IconButton behavior.\n *\n * @category Interactive\n * @platform mobile\n *\n * @example\n * ```tsx\n * import { IconButton } from '@yahoo/uds-mobile/IconButton';\n *\n * <IconButton name=\"Add\" onPress={() => console.log('pressed')} />\n * <IconButton name=\"Close\" variant=\"secondary\" size=\"sm\" />\n * <IconButton name=\"Settings\" loading />\n * ```\n *\n * @usage\n * - Use for toolbar actions\n * - Use for closing modals/dialogs\n * - Always provide accessibilityLabel for screen readers\n *\n * @accessibility\n * - Sets `accessibilityRole=\"button\"` automatically\n * - Announces loading state to screen readers\n * - **Always** provide `accessibilityLabel` since there's no visible text\n *\n * @see {@link Button} for buttons with text labels\n * @see {@link Icon} for non-interactive icons\n */\nconst IconButton = memo(function IconButton({\n name,\n variant = 'primary',\n size = 'md',\n iconVariant = 'outline',\n iconColor,\n loading,\n disabled,\n style,\n accessibilityLabel,\n accessibilityHint,\n disableEffects = false,\n onPressIn,\n onPressOut,\n ref,\n ...props\n}: IconButtonProps) {\n const isDisabled = disabled || loading;\n const shouldAnimate = !disableEffects && !isDisabled;\n const shouldAnimateVariantColors = Platform.OS !== 'web';\n\n const { theme } = useUnistyles();\n const { controlHeight } = useMemo(() => getIconButtonControlMetrics(theme, size), [theme, size]);\n const matchedControlDimensions =\n controlHeight > 0 ? ({ height: controlHeight, width: controlHeight } as const) : undefined;\n\n /* --------------------------------- State ---------------------------------- */\n const [pressed, setPressed] = useState(false);\n\n // On web, useVariants returns the resolved style object instead of mutating\n // the generated styles in place.\n const variantIconButtonStyles = iconButtonStyles.useVariants({ size }) as unknown as\n | typeof iconButtonStyles\n | undefined;\n const resolvedIconButtonStyles = variantIconButtonStyles ?? iconButtonStyles;\n\n const variantButtonStyles = buttonStyles.useVariants({\n variant,\n disabled: isDisabled,\n pressed,\n }) as unknown as typeof buttonStyles | undefined;\n const resolvedButtonStyles = variantButtonStyles ?? buttonStyles;\n\n const variantFoundationStyles = foundationStyles.useVariants({ color: iconColor }) as unknown as\n | typeof foundationStyles\n | undefined;\n const resolvedFoundationStyles = variantFoundationStyles ?? foundationStyles;\n\n const resolvedIconColor = iconColor\n ? (resolvedFoundationStyles.foundation.color as string | undefined)\n : undefined;\n\n // Animate colors using Unistyles' useAnimatedVariantColor\n const backgroundColor = useAnimatedVariantColor(resolvedButtonStyles.root, 'backgroundColor');\n const borderColor = useAnimatedVariantColor(resolvedButtonStyles.root, 'borderColor');\n\n // Get animated theme for boxShadow\n const animatedTheme = useAnimatedTheme();\n\n /* ------------------------------- Animation -------------------------------- */\n const scale = useSharedValue<number>(SCALE_EFFECTS.none);\n\n const handlePressIn = useCallback<NonNullable<PressableProps['onPressIn']>>(\n (event) => {\n setPressed(true);\n if (shouldAnimate) {\n scale.value = withSpring(SCALE_EFFECTS.down, BUTTON_SPRING_CONFIG);\n }\n onPressIn?.(event);\n },\n [shouldAnimate, scale, onPressIn],\n );\n\n const handlePressOut = useCallback<NonNullable<PressableProps['onPressOut']>>(\n (event) => {\n setPressed(false);\n if (shouldAnimate) {\n scale.value = withSpring(SCALE_EFFECTS.none, BUTTON_SPRING_CONFIG);\n }\n onPressOut?.(event);\n },\n [shouldAnimate, scale, onPressOut],\n );\n\n const a11yState = useMemo(() => ({ disabled: isDisabled, busy: loading }), [isDisabled, loading]);\n\n /* --------------------------------- Styles --------------------------------- */\n // Animate pressed state for shadow\n const pressProgress = useDerivedValue(\n () => withTiming(pressed ? 1 : 0, { duration: 220, easing: Easing.bezier(0, 0, 0.2, 1) }),\n [pressed],\n );\n\n // Animate using Unistyles' variant color system + boxShadow from theme\n const animatedRootStyle = useAnimatedStyle(() => {\n // Get boxShadow from theme using flattened path (no camelCase conversion needed!)\n const components = animatedTheme.value.components as unknown as Record<\n string,\n Record<string, unknown>\n >;\n const shadowPressed = components[`button/variant/${variant}/root/pressed`]?.boxShadow as\n | string\n | undefined;\n\n return {\n transform: [{ scale: scale.value }],\n // On web, Unistyles already emits the resolved variant color class. The\n // animated variant color hook can resolve to the black fallback there.\n ...(shouldAnimateVariantColors && {\n backgroundColor: withTiming(backgroundColor.value, {\n duration: 220,\n easing: Easing.bezier(0, 0, 0.2, 1),\n }),\n borderColor: withTiming(borderColor.value, {\n duration: 220,\n easing: Easing.bezier(0, 0, 0.2, 1),\n }),\n }),\n // Only animate shadow if the theme defines one for this variant\n ...(shadowPressed && {\n boxShadow: interpolateShadowAlpha(shadowPressed, pressProgress.value),\n }),\n };\n });\n\n /* --------------------------------- Render --------------------------------- */\n return (\n <AnimatedPressable\n ref={ref}\n disabled={isDisabled}\n onPressIn={handlePressIn}\n onPressOut={handlePressOut}\n flexDirection=\"row\"\n alignItems=\"center\"\n justifyContent=\"center\"\n overflow=\"hidden\"\n accessibilityLabel={loading ? `${accessibilityLabel ?? ''}, loading` : accessibilityLabel}\n accessibilityHint={accessibilityHint}\n accessibilityRole=\"button\"\n accessibilityState={a11yState}\n style={[\n resolvedIconButtonStyles.root,\n resolvedButtonStyles.root,\n matchedControlDimensions,\n resolvedFoundationStyles.foundation,\n animatedRootStyle,\n typeof style === 'function' ? style({ pressed }) : style,\n ]}\n {...props}\n >\n {loading ? (\n <ActivityIndicator\n size={resolvedIconButtonStyles.icon.fontSize}\n color={resolvedIconColor ?? resolvedButtonStyles.icon.color}\n />\n ) : (\n <Icon\n name={name}\n variant={iconVariant}\n style={[resolvedIconButtonStyles.icon, resolvedButtonStyles.icon]}\n dangerouslySetColor={resolvedIconColor}\n />\n )}\n </AnimatedPressable>\n );\n});\n\nIconButton.displayName = 'IconButton';\n\nexport { IconButton, type IconButtonProps };\n"],"mappings":"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"}
|
|
@@ -55,6 +55,8 @@ const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = f
|
|
|
55
55
|
const [internalIsOn, setInternalIsOn] = (0, react.useState)(defaultIsOn);
|
|
56
56
|
const [prefersReducedMotion, setPrefersReducedMotion] = (0, react.useState)(false);
|
|
57
57
|
const isOn = isControlled ? isOnProp : internalIsOn;
|
|
58
|
+
const activeVariant = isOn ? "on" : "off";
|
|
59
|
+
const { theme } = (0, react_native_unistyles.useUnistyles)();
|
|
58
60
|
(0, react.useEffect)(() => {
|
|
59
61
|
const checkReducedMotion = async () => {
|
|
60
62
|
setPrefersReducedMotion(await react_native.AccessibilityInfo.isReduceMotionEnabled());
|
|
@@ -77,11 +79,21 @@ const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = f
|
|
|
77
79
|
isControlled,
|
|
78
80
|
onChange
|
|
79
81
|
]);
|
|
80
|
-
generated_styles.switchStyles.useVariants({
|
|
82
|
+
const resolvedSwitchStyles = generated_styles.switchStyles.useVariants({
|
|
81
83
|
size,
|
|
82
|
-
variant:
|
|
83
|
-
});
|
|
84
|
-
const
|
|
84
|
+
variant: activeVariant
|
|
85
|
+
}) ?? generated_styles.switchStyles;
|
|
86
|
+
const variantLayerStyles = (0, react.useMemo)(() => {
|
|
87
|
+
const components = theme.components;
|
|
88
|
+
const getLayerStyle = (layer) => components[`switch/variant/default/active/${activeVariant}/${layer}/rest`];
|
|
89
|
+
return {
|
|
90
|
+
handle: getLayerStyle("handle"),
|
|
91
|
+
handleIcon: getLayerStyle("handleIcon"),
|
|
92
|
+
switch: getLayerStyle("switch"),
|
|
93
|
+
text: getLayerStyle("rootText")
|
|
94
|
+
};
|
|
95
|
+
}, [activeVariant, theme]);
|
|
96
|
+
const trackBackgroundColor = (0, react_native_unistyles_reanimated.useAnimatedVariantColor)(resolvedSwitchStyles.switch, "backgroundColor");
|
|
85
97
|
const animatedTrackStyle = (0, react_native_reanimated.useAnimatedStyle)(() => {
|
|
86
98
|
"worklet";
|
|
87
99
|
return { backgroundColor: (0, react_native_reanimated.withTiming)(trackBackgroundColor.value, { duration: animationDuration }) };
|
|
@@ -90,17 +102,27 @@ const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = f
|
|
|
90
102
|
"worklet";
|
|
91
103
|
return { transform: [{ translateX: progress.value * travelDistance }] };
|
|
92
104
|
});
|
|
93
|
-
const rootStyle = (0, react.useMemo)(() => [
|
|
105
|
+
const rootStyle = (0, react.useMemo)(() => [resolvedSwitchStyles.root, switchStaticStyles.root({ disabled })], [resolvedSwitchStyles.root, disabled]);
|
|
94
106
|
const trackStyle = (0, react.useMemo)(() => [
|
|
95
|
-
|
|
107
|
+
resolvedSwitchStyles.switch,
|
|
96
108
|
switchStaticStyles.track,
|
|
109
|
+
variantLayerStyles.switch,
|
|
110
|
+
react_native.Platform.OS !== "web" && animatedTrackStyle
|
|
111
|
+
], [
|
|
112
|
+
resolvedSwitchStyles.switch,
|
|
113
|
+
variantLayerStyles.switch,
|
|
97
114
|
animatedTrackStyle
|
|
98
|
-
]
|
|
115
|
+
]);
|
|
99
116
|
const handleStyle = (0, react.useMemo)(() => [
|
|
100
|
-
|
|
117
|
+
resolvedSwitchStyles.handle,
|
|
101
118
|
switchStaticStyles.handle,
|
|
119
|
+
variantLayerStyles.handle,
|
|
102
120
|
animatedHandleStyle
|
|
103
|
-
], [
|
|
121
|
+
], [
|
|
122
|
+
resolvedSwitchStyles.handle,
|
|
123
|
+
variantLayerStyles.handle,
|
|
124
|
+
animatedHandleStyle
|
|
125
|
+
]);
|
|
104
126
|
const accessibilityLabel = typeof label === "string" ? label : void 0;
|
|
105
127
|
const resolvedAccessibilityHint = accessibilityHint ?? "Double tap to toggle";
|
|
106
128
|
const labelContent = label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_FormLabel.FormLabel, {
|
|
@@ -109,7 +131,7 @@ const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = f
|
|
|
109
131
|
label,
|
|
110
132
|
required,
|
|
111
133
|
showRequiredAsterisk: required,
|
|
112
|
-
style:
|
|
134
|
+
style: [resolvedSwitchStyles.text, variantLayerStyles.text]
|
|
113
135
|
});
|
|
114
136
|
const a11yValue = (0, react.useMemo)(() => ({ text: isOn ? "On" : "Off" }), [isOn]);
|
|
115
137
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.Pressable, {
|
|
@@ -139,14 +161,14 @@ const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = f
|
|
|
139
161
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_IconSlot.IconSlot, {
|
|
140
162
|
icon: onIcon,
|
|
141
163
|
variant: "fill",
|
|
142
|
-
style:
|
|
164
|
+
style: [resolvedSwitchStyles.handleIcon, variantLayerStyles.handleIcon]
|
|
143
165
|
})
|
|
144
166
|
}), offIcon && !isOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
|
|
145
167
|
style: switchStaticStyles.iconContainer,
|
|
146
168
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_IconSlot.IconSlot, {
|
|
147
169
|
icon: offIcon,
|
|
148
170
|
variant: "fill",
|
|
149
|
-
style:
|
|
171
|
+
style: [resolvedSwitchStyles.handleIcon, variantLayerStyles.handleIcon]
|
|
150
172
|
})
|
|
151
173
|
})]
|
|
152
174
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Switch.d.cts","names":[],"sources":["../../src/components/Switch.tsx"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"Switch.d.cts","names":[],"sources":["../../src/components/Switch.tsx"],"mappings":";;;;;;;;UAsBU,WAAA,SAAoB,IAAA,CAAK,SAAA,YAAqB,oBAAA,CAAqB,YAAA;;EAE3E,GAAA,GAAM,GAAA,CAAI,IAAA;EAFF;EAIR,QAAA,IAAY,KAAA;;EAEZ,QAAA;EAN2E;EAQ3E,QAAA;EANM;EAQN,iBAAA,GAAoB,kBAAA;AAAA;;;;;;;;;;;;;;;;;;;AAAkB;;;;;;;;;;;;;;;cA2ClC,MAAA,EAAM,OAAA,CAAA,oBAAA,CAAA,WAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Switch.d.ts","names":[],"sources":["../../src/components/Switch.tsx"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"Switch.d.ts","names":[],"sources":["../../src/components/Switch.tsx"],"mappings":";;;;;;;;UAsBU,WAAA,SAAoB,IAAA,CAAK,SAAA,YAAqB,oBAAA,CAAqB,YAAA;;EAE3E,GAAA,GAAM,GAAA,CAAI,IAAA;EAFF;EAIR,QAAA,IAAY,KAAA;;EAEZ,QAAA;EAN2E;EAQ3E,QAAA;EANM;EAQN,iBAAA,GAAoB,kBAAA;AAAA;;;;;;;;;;;;;;;;;;;AAAkB;;;;;;;;;;;;;;;cA2ClC,MAAA,EAAM,OAAA,CAAA,oBAAA,CAAA,WAAA"}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { IconSlot } from "./IconSlot.js";
|
|
3
3
|
import { FormLabel } from "./FormLabel.js";
|
|
4
4
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
5
|
-
import { AccessibilityInfo, Pressable } from "react-native";
|
|
5
|
+
import { AccessibilityInfo, Platform, Pressable } from "react-native";
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { switchStyles } from "../../generated/styles";
|
|
8
|
-
import { StyleSheet as StyleSheet$1 } from "react-native-unistyles";
|
|
8
|
+
import { StyleSheet as StyleSheet$1, useUnistyles } from "react-native-unistyles";
|
|
9
9
|
import Animated, { useAnimatedStyle, useDerivedValue, withTiming } from "react-native-reanimated";
|
|
10
10
|
import { useAnimatedVariantColor } from "react-native-unistyles/reanimated";
|
|
11
11
|
//#region src/components/Switch.tsx
|
|
@@ -52,6 +52,8 @@ const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onCha
|
|
|
52
52
|
const [internalIsOn, setInternalIsOn] = useState(defaultIsOn);
|
|
53
53
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
|
54
54
|
const isOn = isControlled ? isOnProp : internalIsOn;
|
|
55
|
+
const activeVariant = isOn ? "on" : "off";
|
|
56
|
+
const { theme } = useUnistyles();
|
|
55
57
|
useEffect(() => {
|
|
56
58
|
const checkReducedMotion = async () => {
|
|
57
59
|
setPrefersReducedMotion(await AccessibilityInfo.isReduceMotionEnabled());
|
|
@@ -74,11 +76,21 @@ const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onCha
|
|
|
74
76
|
isControlled,
|
|
75
77
|
onChange
|
|
76
78
|
]);
|
|
77
|
-
switchStyles.useVariants({
|
|
79
|
+
const resolvedSwitchStyles = switchStyles.useVariants({
|
|
78
80
|
size,
|
|
79
|
-
variant:
|
|
80
|
-
});
|
|
81
|
-
const
|
|
81
|
+
variant: activeVariant
|
|
82
|
+
}) ?? switchStyles;
|
|
83
|
+
const variantLayerStyles = useMemo(() => {
|
|
84
|
+
const components = theme.components;
|
|
85
|
+
const getLayerStyle = (layer) => components[`switch/variant/default/active/${activeVariant}/${layer}/rest`];
|
|
86
|
+
return {
|
|
87
|
+
handle: getLayerStyle("handle"),
|
|
88
|
+
handleIcon: getLayerStyle("handleIcon"),
|
|
89
|
+
switch: getLayerStyle("switch"),
|
|
90
|
+
text: getLayerStyle("rootText")
|
|
91
|
+
};
|
|
92
|
+
}, [activeVariant, theme]);
|
|
93
|
+
const trackBackgroundColor = useAnimatedVariantColor(resolvedSwitchStyles.switch, "backgroundColor");
|
|
82
94
|
const animatedTrackStyle = useAnimatedStyle(() => {
|
|
83
95
|
"worklet";
|
|
84
96
|
return { backgroundColor: withTiming(trackBackgroundColor.value, { duration: animationDuration }) };
|
|
@@ -87,17 +99,27 @@ const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onCha
|
|
|
87
99
|
"worklet";
|
|
88
100
|
return { transform: [{ translateX: progress.value * travelDistance }] };
|
|
89
101
|
});
|
|
90
|
-
const rootStyle = useMemo(() => [
|
|
102
|
+
const rootStyle = useMemo(() => [resolvedSwitchStyles.root, switchStaticStyles.root({ disabled })], [resolvedSwitchStyles.root, disabled]);
|
|
91
103
|
const trackStyle = useMemo(() => [
|
|
92
|
-
|
|
104
|
+
resolvedSwitchStyles.switch,
|
|
93
105
|
switchStaticStyles.track,
|
|
106
|
+
variantLayerStyles.switch,
|
|
107
|
+
Platform.OS !== "web" && animatedTrackStyle
|
|
108
|
+
], [
|
|
109
|
+
resolvedSwitchStyles.switch,
|
|
110
|
+
variantLayerStyles.switch,
|
|
94
111
|
animatedTrackStyle
|
|
95
|
-
]
|
|
112
|
+
]);
|
|
96
113
|
const handleStyle = useMemo(() => [
|
|
97
|
-
|
|
114
|
+
resolvedSwitchStyles.handle,
|
|
98
115
|
switchStaticStyles.handle,
|
|
116
|
+
variantLayerStyles.handle,
|
|
99
117
|
animatedHandleStyle
|
|
100
|
-
], [
|
|
118
|
+
], [
|
|
119
|
+
resolvedSwitchStyles.handle,
|
|
120
|
+
variantLayerStyles.handle,
|
|
121
|
+
animatedHandleStyle
|
|
122
|
+
]);
|
|
101
123
|
const accessibilityLabel = typeof label === "string" ? label : void 0;
|
|
102
124
|
const resolvedAccessibilityHint = accessibilityHint ?? "Double tap to toggle";
|
|
103
125
|
const labelContent = label && /* @__PURE__ */ jsx(FormLabel, {
|
|
@@ -106,7 +128,7 @@ const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onCha
|
|
|
106
128
|
label,
|
|
107
129
|
required,
|
|
108
130
|
showRequiredAsterisk: required,
|
|
109
|
-
style:
|
|
131
|
+
style: [resolvedSwitchStyles.text, variantLayerStyles.text]
|
|
110
132
|
});
|
|
111
133
|
const a11yValue = useMemo(() => ({ text: isOn ? "On" : "Off" }), [isOn]);
|
|
112
134
|
return /* @__PURE__ */ jsxs(Pressable, {
|
|
@@ -136,14 +158,14 @@ const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onCha
|
|
|
136
158
|
children: /* @__PURE__ */ jsx(IconSlot, {
|
|
137
159
|
icon: onIcon,
|
|
138
160
|
variant: "fill",
|
|
139
|
-
style:
|
|
161
|
+
style: [resolvedSwitchStyles.handleIcon, variantLayerStyles.handleIcon]
|
|
140
162
|
})
|
|
141
163
|
}), offIcon && !isOn && /* @__PURE__ */ jsx(Animated.View, {
|
|
142
164
|
style: switchStaticStyles.iconContainer,
|
|
143
165
|
children: /* @__PURE__ */ jsx(IconSlot, {
|
|
144
166
|
icon: offIcon,
|
|
145
167
|
variant: "fill",
|
|
146
|
-
style:
|
|
168
|
+
style: [resolvedSwitchStyles.handleIcon, variantLayerStyles.handleIcon]
|
|
147
169
|
})
|
|
148
170
|
})]
|
|
149
171
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Switch.js","names":["StyleSheet"],"sources":["../../src/components/Switch.tsx"],"sourcesContent":["import type { SwitchSize, UniversalSwitchProps } from '@yahoo/uds-types';\nimport type { Ref } from 'react';\nimport { memo, useCallback, useEffect, useMemo, useState } from 'react';\nimport type { AccessibilityProps, StyleProp, View, ViewProps, ViewStyle } from 'react-native';\nimport { AccessibilityInfo, Pressable } from 'react-native';\nimport Animated, { useAnimatedStyle, useDerivedValue, withTiming } from 'react-native-reanimated';\nimport { StyleSheet } from 'react-native-unistyles';\nimport { useAnimatedVariantColor } from 'react-native-unistyles/reanimated';\n\nimport { switchStyles } from '../../generated/styles';\nimport { FormLabel } from './FormLabel';\nimport type { IconSlotType } from './IconSlot';\nimport { IconSlot } from './IconSlot';\n\ninterface SwitchProps extends Omit<ViewProps, 'style'>, UniversalSwitchProps<IconSlotType> {\n /** Ref to the underlying View */\n ref?: Ref<View>;\n /** Callback when the switch value changes */\n onChange?: (value: boolean) => void;\n /** Whether the switch is disabled */\n disabled?: boolean;\n /** Whether the switch is required (shows asterisk with label) */\n required?: boolean;\n /** Accessibility hint describing what happens when activated */\n accessibilityHint?: AccessibilityProps['accessibilityHint'];\n}\n\nconst HANDLE_TRAVEL: Record<SwitchSize, number> = {\n md: 20,\n sm: 12,\n};\n\nconst ANIMATION_DURATION = 120;\n\n/**\n * **Switch component for toggling options**\n *\n * @description\n * A switch (also called a toggle) is a binary on/off input control.\n * It allows users to pick between two clearly opposite choices.\n *\n * @category Form\n * @platform mobile\n *\n * @example\n * ```tsx\n * import { Switch } from '@yahoo/uds-mobile/Switch';\n *\n * <Switch label=\"Notifications\" />\n * <Switch isOn={enabled} onChange={setEnabled} label=\"Dark mode\" />\n * <Switch onIcon=\"Check\" offIcon=\"Cross\" label=\"Sync\" />\n * ```\n *\n * @usage\n * - Settings: For toggling preferences on/off\n * - Feature flags: For enabling/disabling features\n * - Immediate effect toggles (no submit button needed)\n *\n * @accessibility\n * - Sets `accessibilityRole=\"switch\"` automatically\n * - Announces on/off state to screen readers\n * - Respects system reduce motion preference\n * - Supports `reduceMotion` prop to disable animations\n *\n * @see {@link Checkbox} for forms with submit actions\n * @see {@link Radio} for single-select options\n */\nconst Switch = memo(function Switch({\n isOn: isOnProp,\n defaultIsOn = false,\n onChange,\n label,\n labelPosition = 'start',\n size = 'md',\n onIcon,\n offIcon,\n disabled = false,\n required,\n accessibilityHint,\n reduceMotion = false,\n ref,\n ...viewProps\n}: SwitchProps) {\n const isControlled = isOnProp !== undefined;\n const [internalIsOn, setInternalIsOn] = useState(defaultIsOn);\n const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);\n const isOn = isControlled ? isOnProp : internalIsOn;\n\n // Check system reduced motion preference\n useEffect(() => {\n const checkReducedMotion = async () => {\n const isReduceMotionEnabled = await AccessibilityInfo.isReduceMotionEnabled();\n setPrefersReducedMotion(isReduceMotionEnabled);\n };\n checkReducedMotion();\n\n const subscription = AccessibilityInfo.addEventListener(\n 'reduceMotionChanged',\n setPrefersReducedMotion,\n );\n return () => subscription.remove();\n }, []);\n\n const shouldReduceMotion = reduceMotion || prefersReducedMotion;\n const animationDuration = shouldReduceMotion ? 0 : ANIMATION_DURATION;\n\n const progress = useDerivedValue(\n () => withTiming(isOn ? 1 : 0, { duration: animationDuration }),\n [isOn, animationDuration],\n );\n\n const travelDistance = HANDLE_TRAVEL[size];\n\n const handlePress = useCallback(() => {\n if (disabled) {\n return;\n }\n\n const newValue = !isOn;\n\n if (!isControlled) {\n setInternalIsOn(newValue);\n }\n\n onChange?.(newValue);\n }, [disabled, isOn, isControlled, onChange]);\n\n switchStyles.useVariants({\n size,\n variant: isOn ? 'on' : 'off',\n });\n\n // Get animated track color from design tokens (changes when variant changes)\n const trackBackgroundColor = useAnimatedVariantColor(switchStyles.switch, 'backgroundColor');\n\n const animatedTrackStyle = useAnimatedStyle(() => {\n 'worklet';\n return {\n backgroundColor: withTiming(trackBackgroundColor.value, { duration: animationDuration }),\n };\n });\n\n const animatedHandleStyle = useAnimatedStyle(() => {\n 'worklet';\n return {\n transform: [{ translateX: progress.value * travelDistance }],\n };\n });\n\n const rootStyle: StyleProp<ViewStyle> = useMemo(\n () => [switchStyles.root, switchStaticStyles.root({ disabled })],\n [switchStyles.root, disabled],\n );\n\n const trackStyle: StyleProp<ViewStyle> = useMemo(\n () => [switchStyles.switch, switchStaticStyles.track, animatedTrackStyle],\n [switchStyles.switch, animatedTrackStyle],\n );\n\n const handleStyle: StyleProp<ViewStyle> = useMemo(\n () => [switchStyles.handle, switchStaticStyles.handle, animatedHandleStyle],\n [switchStyles.handle, animatedHandleStyle],\n );\n\n const accessibilityLabel = typeof label === 'string' ? label : undefined;\n const resolvedAccessibilityHint = accessibilityHint ?? 'Double tap to toggle';\n\n const labelContent = label && (\n <FormLabel\n color=\"inherit\"\n variant=\"inherit\"\n label={label}\n required={required}\n showRequiredAsterisk={required}\n style={switchStyles.text}\n />\n );\n\n const a11yValue = useMemo(() => ({ text: isOn ? 'On' : 'Off' }), [isOn]);\n\n return (\n <Pressable\n ref={ref}\n onPress={handlePress}\n disabled={disabled}\n accessible\n accessibilityRole=\"switch\"\n accessibilityState={{ checked: isOn, disabled }}\n accessibilityLabel={accessibilityLabel}\n accessibilityHint={resolvedAccessibilityHint}\n accessibilityValue={a11yValue}\n {...viewProps}\n style={rootStyle}\n >\n {labelPosition === 'start' && labelContent}\n\n <Animated.View style={trackStyle} importantForAccessibility=\"no-hide-descendants\">\n <Animated.View style={handleStyle}>\n {onIcon && isOn && (\n <Animated.View style={switchStaticStyles.iconContainer}>\n <IconSlot icon={onIcon} variant=\"fill\" style={switchStyles.handleIcon} />\n </Animated.View>\n )}\n {offIcon && !isOn && (\n <Animated.View style={switchStaticStyles.iconContainer}>\n <IconSlot icon={offIcon} variant=\"fill\" style={switchStyles.handleIcon} />\n </Animated.View>\n )}\n </Animated.View>\n </Animated.View>\n\n {labelPosition === 'end' && labelContent}\n </Pressable>\n );\n});\n\nSwitch.displayName = 'Switch';\n\nconst switchStaticStyles = StyleSheet.create((theme) => ({\n handle: {\n borderRadius: theme.borderRadius.full,\n alignItems: 'center',\n justifyContent: 'center',\n },\n iconContainer: {\n position: 'absolute',\n alignItems: 'center',\n justifyContent: 'center',\n },\n track: {\n justifyContent: 'center',\n borderRadius: theme.borderRadius.full,\n },\n root: ({ disabled }: { disabled: boolean }) => ({\n flexDirection: 'row',\n alignItems: 'center',\n alignSelf: 'flex-start',\n opacity: disabled ? 0.5 : 1,\n }),\n}));\n\nexport { Switch, type SwitchProps };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Switch.js","names":["StyleSheet"],"sources":["../../src/components/Switch.tsx"],"sourcesContent":["import type { SwitchSize, UniversalSwitchProps } from '@yahoo/uds-types';\nimport type { Ref } from 'react';\nimport { memo, useCallback, useEffect, useMemo, useState } from 'react';\nimport type {\n AccessibilityProps,\n StyleProp,\n TextStyle,\n View,\n ViewProps,\n ViewStyle,\n} from 'react-native';\nimport { AccessibilityInfo, Platform, Pressable } from 'react-native';\nimport Animated, { useAnimatedStyle, useDerivedValue, withTiming } from 'react-native-reanimated';\n// eslint-disable-next-line uds/no-use-unistyles -- switch variant layers need concrete web styles\nimport { StyleSheet, useUnistyles } from 'react-native-unistyles';\nimport { useAnimatedVariantColor } from 'react-native-unistyles/reanimated';\n\nimport { switchStyles } from '../../generated/styles';\nimport { FormLabel } from './FormLabel';\nimport type { IconSlotType } from './IconSlot';\nimport { IconSlot } from './IconSlot';\n\ninterface SwitchProps extends Omit<ViewProps, 'style'>, UniversalSwitchProps<IconSlotType> {\n /** Ref to the underlying View */\n ref?: Ref<View>;\n /** Callback when the switch value changes */\n onChange?: (value: boolean) => void;\n /** Whether the switch is disabled */\n disabled?: boolean;\n /** Whether the switch is required (shows asterisk with label) */\n required?: boolean;\n /** Accessibility hint describing what happens when activated */\n accessibilityHint?: AccessibilityProps['accessibilityHint'];\n}\n\nconst HANDLE_TRAVEL: Record<SwitchSize, number> = {\n md: 20,\n sm: 12,\n};\n\nconst ANIMATION_DURATION = 120;\n\n/**\n * **Switch component for toggling options**\n *\n * @description\n * A switch (also called a toggle) is a binary on/off input control.\n * It allows users to pick between two clearly opposite choices.\n *\n * @category Form\n * @platform mobile\n *\n * @example\n * ```tsx\n * import { Switch } from '@yahoo/uds-mobile/Switch';\n *\n * <Switch label=\"Notifications\" />\n * <Switch isOn={enabled} onChange={setEnabled} label=\"Dark mode\" />\n * <Switch onIcon=\"Check\" offIcon=\"Cross\" label=\"Sync\" />\n * ```\n *\n * @usage\n * - Settings: For toggling preferences on/off\n * - Feature flags: For enabling/disabling features\n * - Immediate effect toggles (no submit button needed)\n *\n * @accessibility\n * - Sets `accessibilityRole=\"switch\"` automatically\n * - Announces on/off state to screen readers\n * - Respects system reduce motion preference\n * - Supports `reduceMotion` prop to disable animations\n *\n * @see {@link Checkbox} for forms with submit actions\n * @see {@link Radio} for single-select options\n */\nconst Switch = memo(function Switch({\n isOn: isOnProp,\n defaultIsOn = false,\n onChange,\n label,\n labelPosition = 'start',\n size = 'md',\n onIcon,\n offIcon,\n disabled = false,\n required,\n accessibilityHint,\n reduceMotion = false,\n ref,\n ...viewProps\n}: SwitchProps) {\n const isControlled = isOnProp !== undefined;\n const [internalIsOn, setInternalIsOn] = useState(defaultIsOn);\n const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);\n const isOn = isControlled ? isOnProp : internalIsOn;\n const activeVariant = isOn ? 'on' : 'off';\n const { theme } = useUnistyles();\n\n // Check system reduced motion preference\n useEffect(() => {\n const checkReducedMotion = async () => {\n const isReduceMotionEnabled = await AccessibilityInfo.isReduceMotionEnabled();\n setPrefersReducedMotion(isReduceMotionEnabled);\n };\n checkReducedMotion();\n\n const subscription = AccessibilityInfo.addEventListener(\n 'reduceMotionChanged',\n setPrefersReducedMotion,\n );\n return () => subscription.remove();\n }, []);\n\n const shouldReduceMotion = reduceMotion || prefersReducedMotion;\n const animationDuration = shouldReduceMotion ? 0 : ANIMATION_DURATION;\n\n const progress = useDerivedValue(\n () => withTiming(isOn ? 1 : 0, { duration: animationDuration }),\n [isOn, animationDuration],\n );\n\n const travelDistance = HANDLE_TRAVEL[size];\n\n const handlePress = useCallback(() => {\n if (disabled) {\n return;\n }\n\n const newValue = !isOn;\n\n if (!isControlled) {\n setInternalIsOn(newValue);\n }\n\n onChange?.(newValue);\n }, [disabled, isOn, isControlled, onChange]);\n\n // On web, useVariants returns the resolved style object instead of mutating\n // switchStyles in place.\n const variantSwitchStyles = switchStyles.useVariants({\n size,\n variant: activeVariant,\n }) as unknown as typeof switchStyles | undefined;\n const resolvedSwitchStyles = variantSwitchStyles ?? switchStyles;\n\n const variantLayerStyles = useMemo(() => {\n const components = theme.components as unknown as Record<string, Record<string, unknown>>;\n const getLayerStyle = <TStyle,>(layer: string) =>\n components[`switch/variant/default/active/${activeVariant}/${layer}/rest`] as\n | TStyle\n | undefined;\n\n return {\n handle: getLayerStyle<ViewStyle>('handle'),\n handleIcon: getLayerStyle<TextStyle>('handleIcon'),\n switch: getLayerStyle<ViewStyle>('switch'),\n text: getLayerStyle<TextStyle>('rootText'),\n };\n }, [activeVariant, theme]);\n\n // Get animated track color from design tokens (changes when variant changes)\n const trackBackgroundColor = useAnimatedVariantColor(\n resolvedSwitchStyles.switch,\n 'backgroundColor',\n );\n\n const animatedTrackStyle = useAnimatedStyle(() => {\n 'worklet';\n return {\n backgroundColor: withTiming(trackBackgroundColor.value, { duration: animationDuration }),\n };\n });\n\n const animatedHandleStyle = useAnimatedStyle(() => {\n 'worklet';\n return {\n transform: [{ translateX: progress.value * travelDistance }],\n };\n });\n\n const rootStyle: StyleProp<ViewStyle> = useMemo(\n () => [resolvedSwitchStyles.root, switchStaticStyles.root({ disabled })],\n [resolvedSwitchStyles.root, disabled],\n );\n\n const trackStyle: StyleProp<ViewStyle> = useMemo(\n () => [\n resolvedSwitchStyles.switch,\n switchStaticStyles.track,\n variantLayerStyles.switch,\n // On web, the animated variant color hook currently resolves to Unistyles'\n // black fallback, so the concrete variant layer provides the track color.\n Platform.OS !== 'web' && animatedTrackStyle,\n ],\n [resolvedSwitchStyles.switch, variantLayerStyles.switch, animatedTrackStyle],\n );\n\n const handleStyle: StyleProp<ViewStyle> = useMemo(\n () => [\n resolvedSwitchStyles.handle,\n switchStaticStyles.handle,\n variantLayerStyles.handle,\n animatedHandleStyle,\n ],\n [resolvedSwitchStyles.handle, variantLayerStyles.handle, animatedHandleStyle],\n );\n\n const accessibilityLabel = typeof label === 'string' ? label : undefined;\n const resolvedAccessibilityHint = accessibilityHint ?? 'Double tap to toggle';\n\n const labelContent = label && (\n <FormLabel\n color=\"inherit\"\n variant=\"inherit\"\n label={label}\n required={required}\n showRequiredAsterisk={required}\n style={[resolvedSwitchStyles.text, variantLayerStyles.text]}\n />\n );\n\n const a11yValue = useMemo(() => ({ text: isOn ? 'On' : 'Off' }), [isOn]);\n\n return (\n <Pressable\n ref={ref}\n onPress={handlePress}\n disabled={disabled}\n accessible\n accessibilityRole=\"switch\"\n accessibilityState={{ checked: isOn, disabled }}\n accessibilityLabel={accessibilityLabel}\n accessibilityHint={resolvedAccessibilityHint}\n accessibilityValue={a11yValue}\n {...viewProps}\n style={rootStyle}\n >\n {labelPosition === 'start' && labelContent}\n\n <Animated.View style={trackStyle} importantForAccessibility=\"no-hide-descendants\">\n <Animated.View style={handleStyle}>\n {onIcon && isOn && (\n <Animated.View style={switchStaticStyles.iconContainer}>\n <IconSlot\n icon={onIcon}\n variant=\"fill\"\n style={[resolvedSwitchStyles.handleIcon, variantLayerStyles.handleIcon]}\n />\n </Animated.View>\n )}\n {offIcon && !isOn && (\n <Animated.View style={switchStaticStyles.iconContainer}>\n <IconSlot\n icon={offIcon}\n variant=\"fill\"\n style={[resolvedSwitchStyles.handleIcon, variantLayerStyles.handleIcon]}\n />\n </Animated.View>\n )}\n </Animated.View>\n </Animated.View>\n\n {labelPosition === 'end' && labelContent}\n </Pressable>\n );\n});\n\nSwitch.displayName = 'Switch';\n\nconst switchStaticStyles = StyleSheet.create((theme) => ({\n handle: {\n borderRadius: theme.borderRadius.full,\n alignItems: 'center',\n justifyContent: 'center',\n },\n iconContainer: {\n position: 'absolute',\n alignItems: 'center',\n justifyContent: 'center',\n },\n track: {\n justifyContent: 'center',\n borderRadius: theme.borderRadius.full,\n },\n root: ({ disabled }: { disabled: boolean }) => ({\n flexDirection: 'row',\n alignItems: 'center',\n alignSelf: 'flex-start',\n opacity: disabled ? 0.5 : 1,\n }),\n}));\n\nexport { Switch, type SwitchProps };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/*! © 2026 Yahoo, Inc. UDS Mobile v0.0.0-development */
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const require_components_UDSProvider = require("./UDSProvider.cjs");
|
|
4
|
+
exports.UDSGestureProvider = require_components_UDSProvider.UDSGestureProvider;
|
|
@@ -8,29 +8,34 @@ let react_jsx_runtime = require("react/jsx-runtime");
|
|
|
8
8
|
let react_native_gesture_handler = require("react-native-gesture-handler");
|
|
9
9
|
//#region src/components/UDSProvider.tsx
|
|
10
10
|
/**
|
|
11
|
-
* Root provider for UDS Mobile.
|
|
11
|
+
* Root gesture and portal provider for UDS Mobile overlays.
|
|
12
12
|
*
|
|
13
13
|
* Place this at the top of your app layout:
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
16
16
|
* ```tsx
|
|
17
|
-
* import {
|
|
17
|
+
* import { UDSGestureProvider } from '@yahoo/uds-mobile/UDSGestureProvider';
|
|
18
18
|
*
|
|
19
19
|
* export default function RootLayout() {
|
|
20
20
|
* return (
|
|
21
|
-
* <
|
|
21
|
+
* <UDSGestureProvider>
|
|
22
22
|
* <Stack />
|
|
23
|
-
* </
|
|
23
|
+
* </UDSGestureProvider>
|
|
24
24
|
* );
|
|
25
25
|
* }
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
|
-
const
|
|
28
|
+
const UDSGestureProvider = (0, react.memo)(function UDSGestureProvider({ children }) {
|
|
29
29
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_gesture_handler.GestureHandlerRootView, {
|
|
30
30
|
style: styles.root,
|
|
31
31
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_portal.PortalProvider, { children })
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Use {@link UDSGestureProvider} from `@yahoo/uds-mobile/UDSGestureProvider`.
|
|
36
|
+
*/
|
|
37
|
+
const UDSProvider = UDSGestureProvider;
|
|
34
38
|
const styles = react_native.StyleSheet.create({ root: { flex: 1 } });
|
|
35
39
|
//#endregion
|
|
40
|
+
exports.UDSGestureProvider = UDSGestureProvider;
|
|
36
41
|
exports.UDSProvider = UDSProvider;
|