@yahoo/uds-mobile 2.23.2 → 2.24.0-beta.1
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 +4 -12
- package/dist/bin/automated-config/dist/generated/generatedConfigs.mjs +92 -46
- package/dist/components/Button/buttonTheme.cjs +63 -23
- package/dist/components/Button/buttonTheme.d.cts +29 -12
- package/dist/components/Button/buttonTheme.d.cts.map +1 -1
- package/dist/components/Button/buttonTheme.d.ts +29 -12
- package/dist/components/Button/buttonTheme.d.ts.map +1 -1
- package/dist/components/Button/buttonTheme.js +62 -23
- package/dist/components/Button/buttonTheme.js.map +1 -1
- package/dist/components/Button.cjs +34 -13
- package/dist/components/Button.d.cts +7 -0
- package/dist/components/Button.d.cts.map +1 -1
- package/dist/components/Button.d.ts +7 -0
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +34 -13
- package/dist/components/Button.js.map +1 -1
- package/dist/components/Checkbox.cjs +30 -7
- package/dist/components/Checkbox.d.cts +7 -0
- package/dist/components/Checkbox.d.cts.map +1 -1
- package/dist/components/Checkbox.d.ts +7 -0
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox.js +30 -7
- package/dist/components/Checkbox.js.map +1 -1
- package/dist/components/Chip/ChipBase.cjs +23 -9
- package/dist/components/Chip/ChipBase.d.cts.map +1 -1
- package/dist/components/Chip/ChipBase.d.ts.map +1 -1
- package/dist/components/Chip/ChipBase.js +23 -9
- package/dist/components/Chip/ChipBase.js.map +1 -1
- package/dist/components/Chip/ChipDismissible.cjs +3 -1
- package/dist/components/Chip/ChipDismissible.js +3 -1
- package/dist/components/Chip/ChipDismissible.js.map +1 -1
- package/dist/components/Chip/types.d.cts +7 -0
- package/dist/components/Chip/types.d.cts.map +1 -1
- package/dist/components/Chip/types.d.ts +7 -0
- package/dist/components/Chip/types.d.ts.map +1 -1
- package/dist/components/Icon.cjs +7 -3
- package/dist/components/Icon.d.cts +9 -1
- package/dist/components/Icon.d.cts.map +1 -1
- package/dist/components/Icon.d.ts +9 -1
- package/dist/components/Icon.d.ts.map +1 -1
- package/dist/components/Icon.js +7 -3
- package/dist/components/Icon.js.map +1 -1
- package/dist/components/IconButton.cjs +18 -8
- package/dist/components/IconButton.js +18 -8
- package/dist/components/IconButton.js.map +1 -1
- package/dist/components/Radio/Radio.cjs +29 -11
- package/dist/components/Radio/Radio.d.cts +7 -0
- package/dist/components/Radio/Radio.d.cts.map +1 -1
- package/dist/components/Radio/Radio.d.ts +7 -0
- package/dist/components/Radio/Radio.d.ts.map +1 -1
- package/dist/components/Radio/Radio.js +29 -11
- package/dist/components/Radio/Radio.js.map +1 -1
- package/dist/components/Switch.cjs +25 -2
- package/dist/components/Switch.d.cts +7 -0
- package/dist/components/Switch.d.cts.map +1 -1
- package/dist/components/Switch.d.ts +7 -0
- package/dist/components/Switch.d.ts.map +1 -1
- package/dist/components/Switch.js +25 -2
- package/dist/components/Switch.js.map +1 -1
- package/dist/components/Tabs/Tab.cjs +12 -2
- package/dist/components/Tabs/Tab.d.cts.map +1 -1
- package/dist/components/Tabs/Tab.d.ts.map +1 -1
- package/dist/components/Tabs/Tab.js +12 -2
- package/dist/components/Tabs/Tab.js.map +1 -1
- package/dist/components/Tabs/Tabs.cjs +8 -3
- package/dist/components/Tabs/Tabs.d.cts +9 -1
- package/dist/components/Tabs/Tabs.d.cts.map +1 -1
- package/dist/components/Tabs/Tabs.d.ts +9 -1
- package/dist/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/components/Tabs/Tabs.js +8 -3
- package/dist/components/Tabs/Tabs.js.map +1 -1
- package/dist/components/Tabs/tabsContexts.d.cts +2 -0
- package/dist/components/Tabs/tabsContexts.d.cts.map +1 -1
- package/dist/components/Tabs/tabsContexts.d.ts +2 -0
- package/dist/components/Tabs/tabsContexts.d.ts.map +1 -1
- package/dist/components/Tabs/tabsContexts.js.map +1 -1
- package/dist/components/Text.cjs +4 -1
- package/dist/components/Text.d.cts +10 -2
- package/dist/components/Text.d.cts.map +1 -1
- package/dist/components/Text.d.ts +10 -2
- package/dist/components/Text.d.ts.map +1 -1
- package/dist/components/Text.js +4 -1
- package/dist/components/Text.js.map +1 -1
- package/dist/components/UDSProvider.cjs +6 -2
- package/dist/components/UDSProvider.d.cts +8 -0
- package/dist/components/UDSProvider.d.cts.map +1 -1
- package/dist/components/UDSProvider.d.ts +8 -0
- package/dist/components/UDSProvider.d.ts.map +1 -1
- package/dist/components/UDSProvider.js +6 -2
- package/dist/components/UDSProvider.js.map +1 -1
- package/dist/fontScaling/FontScalingContext.cjs +35 -0
- package/dist/fontScaling/FontScalingContext.d.cts +38 -0
- package/dist/fontScaling/FontScalingContext.d.cts.map +1 -0
- package/dist/fontScaling/FontScalingContext.d.ts +38 -0
- package/dist/fontScaling/FontScalingContext.d.ts.map +1 -0
- package/dist/fontScaling/FontScalingContext.js +34 -0
- package/dist/fontScaling/FontScalingContext.js.map +1 -0
- package/dist/fontScaling/constants.cjs +72 -0
- package/dist/fontScaling/constants.d.cts +49 -0
- package/dist/fontScaling/constants.d.cts.map +1 -0
- package/dist/fontScaling/constants.d.ts +49 -0
- package/dist/fontScaling/constants.d.ts.map +1 -0
- package/dist/fontScaling/constants.js +70 -0
- package/dist/fontScaling/constants.js.map +1 -0
- package/dist/fontScaling/index.cjs +13 -0
- package/dist/fontScaling/index.d.cts +8 -0
- package/dist/fontScaling/index.d.ts +8 -0
- package/dist/fontScaling/index.js +6 -0
- package/dist/fontScaling/resolveMaxFontSizeMultiplier.cjs +38 -0
- package/dist/fontScaling/resolveMaxFontSizeMultiplier.d.cts +36 -0
- package/dist/fontScaling/resolveMaxFontSizeMultiplier.d.cts.map +1 -0
- package/dist/fontScaling/resolveMaxFontSizeMultiplier.d.ts +36 -0
- package/dist/fontScaling/resolveMaxFontSizeMultiplier.d.ts.map +1 -0
- package/dist/fontScaling/resolveMaxFontSizeMultiplier.js +39 -0
- package/dist/fontScaling/resolveMaxFontSizeMultiplier.js.map +1 -0
- package/dist/fontScaling/types.cjs +1 -0
- package/dist/fontScaling/types.d.cts +51 -0
- package/dist/fontScaling/types.d.cts.map +1 -0
- package/dist/fontScaling/types.d.ts +51 -0
- package/dist/fontScaling/types.d.ts.map +1 -0
- package/dist/fontScaling/types.js +1 -0
- package/dist/fontScaling/useFontScale.cjs +32 -0
- package/dist/fontScaling/useFontScale.d.cts +18 -0
- package/dist/fontScaling/useFontScale.d.cts.map +1 -0
- package/dist/fontScaling/useFontScale.d.ts +18 -0
- package/dist/fontScaling/useFontScale.d.ts.map +1 -0
- package/dist/fontScaling/useFontScale.js +32 -0
- package/dist/fontScaling/useFontScale.js.map +1 -0
- package/dist/fontScaling/useMaxFontSizeMultiplier.cjs +24 -0
- package/dist/fontScaling/useMaxFontSizeMultiplier.d.cts +16 -0
- package/dist/fontScaling/useMaxFontSizeMultiplier.d.cts.map +1 -0
- package/dist/fontScaling/useMaxFontSizeMultiplier.d.ts +16 -0
- package/dist/fontScaling/useMaxFontSizeMultiplier.d.ts.map +1 -0
- package/dist/fontScaling/useMaxFontSizeMultiplier.js +24 -0
- package/dist/fontScaling/useMaxFontSizeMultiplier.js.map +1 -0
- package/dist/jest/mocks/react-native.cjs +19 -4
- package/dist/jest/mocks/react-native.d.cts +8 -5
- package/dist/jest/mocks/react-native.d.cts.map +1 -1
- package/dist/jest/mocks/react-native.d.ts +8 -5
- package/dist/jest/mocks/react-native.d.ts.map +1 -1
- package/dist/jest/mocks/react-native.js +18 -5
- package/dist/jest/mocks/react-native.js.map +1 -1
- package/dist/jest/mocks/styles.cjs +14 -0
- 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 +14 -0
- package/dist/jest/mocks/styles.js.map +1 -1
- package/dist/jest/setup.cjs +4 -0
- package/dist/jest/setup.d.cts.map +1 -1
- package/dist/jest/setup.d.ts.map +1 -1
- package/dist/jest/setup.js +4 -0
- package/dist/jest/setup.js.map +1 -1
- package/dist/mobile/generated/fontMetrics.cjs +1166 -0
- package/dist/mobile/generated/fontMetrics.js +1168 -0
- package/dist/mobile/generated/fontMetrics.js.map +1 -0
- package/dist/portal.d.cts.map +1 -1
- package/dist/portal.d.ts.map +1 -1
- package/dist/portal.js.map +1 -1
- package/dist/types/dist/index.d.cts.map +1 -1
- package/dist/types/dist/index.d.ts.map +1 -1
- package/fonts/uds-icons.ttf +0 -0
- package/generated/fontMetrics.ts +1173 -0
- package/generated/styles.d.ts +5 -28
- package/generated/unistyles.d.ts +0 -27
- package/package.json +11 -1
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
|
|
4
4
|
const require_index = require("../../motion-tokens/dist/index.cjs");
|
|
5
5
|
const require_motion = require("../../motion.cjs");
|
|
6
|
+
const require_fontScaling_useMaxFontSizeMultiplier = require("../../fontScaling/useMaxFontSizeMultiplier.cjs");
|
|
7
|
+
const require_fontScaling_useFontScale = require("../../fontScaling/useFontScale.cjs");
|
|
6
8
|
const require_components_Pressable = require("../Pressable.cjs");
|
|
7
9
|
const require_components_FormLabel = require("../FormLabel.cjs");
|
|
8
10
|
const require_components_Radio_RadioGroupContext = require("./RadioGroupContext.cjs");
|
|
@@ -49,9 +51,11 @@ const VARIANT_ERROR_MAP = {
|
|
|
49
51
|
* @see {@link Checkbox} for multi-select options
|
|
50
52
|
* @see {@link Switch} for binary toggles
|
|
51
53
|
*/
|
|
52
|
-
const Radio = (0, react.memo)(function Radio({ label, labelPosition = "start", variant: variantInput, size: sizeInput, value: valueProp, checked: checkedProp, hasError: hasErrorProp, reduceMotion: reduceMotionProp, defaultChecked, disabled: disabledProp, required, onChange, ref, ...viewProps }) {
|
|
54
|
+
const Radio = (0, react.memo)(function Radio({ label, labelPosition = "start", variant: variantInput, size: sizeInput, value: valueProp, checked: checkedProp, hasError: hasErrorProp, reduceMotion: reduceMotionProp, defaultChecked, disabled: disabledProp, required, maxFontSizeMultiplier, onChange, ref, ...viewProps }) {
|
|
53
55
|
const uid = `uds-radio-${(0, react.useId)()}`;
|
|
54
56
|
const radioGroup = require_components_Radio_RadioGroupContext.useRadioGroupContext();
|
|
57
|
+
const resolvedMaxFontSizeMultiplier = require_fontScaling_useMaxFontSizeMultiplier.useMaxFontSizeMultiplier("control", maxFontSizeMultiplier) ?? 0;
|
|
58
|
+
const fontScaleFactor = require_fontScaling_useFontScale.useFontScale(resolvedMaxFontSizeMultiplier);
|
|
55
59
|
const disabled = Boolean(disabledProp || radioGroup?.disabled);
|
|
56
60
|
const hasError = hasErrorProp ?? radioGroup?.hasError;
|
|
57
61
|
const reduceMotion = reduceMotionProp ?? radioGroup?.reduceMotion;
|
|
@@ -107,21 +111,33 @@ const Radio = (0, react.memo)(function Radio({ label, labelPosition = "start", v
|
|
|
107
111
|
pressed
|
|
108
112
|
});
|
|
109
113
|
const rootStyle = (0, react.useMemo)(() => [generated_styles.radioStyles.root, { opacity: disabled ? .5 : 1 }], [generated_styles.radioStyles.root, disabled]);
|
|
110
|
-
const radioBoxStyle = (0, react.useMemo)(() =>
|
|
114
|
+
const radioBoxStyle = (0, react.useMemo)(() => {
|
|
115
|
+
const { width, height } = generated_styles.radioStyles.radio;
|
|
116
|
+
const scaledBox = fontScaleFactor !== 1 && typeof width === "number" && typeof height === "number" ? {
|
|
117
|
+
width: Math.round(width * fontScaleFactor),
|
|
118
|
+
height: Math.round(height * fontScaleFactor)
|
|
119
|
+
} : void 0;
|
|
120
|
+
return [
|
|
121
|
+
generated_styles.radioStyles.radio,
|
|
122
|
+
scaledBox,
|
|
123
|
+
{
|
|
124
|
+
alignItems: "center",
|
|
125
|
+
justifyContent: "center",
|
|
126
|
+
borderRadius: 9999
|
|
127
|
+
},
|
|
128
|
+
radioAnimatedStyle
|
|
129
|
+
];
|
|
130
|
+
}, [
|
|
111
131
|
generated_styles.radioStyles.radio,
|
|
112
|
-
|
|
113
|
-
alignItems: "center",
|
|
114
|
-
justifyContent: "center",
|
|
115
|
-
borderRadius: 9999
|
|
116
|
-
},
|
|
132
|
+
fontScaleFactor,
|
|
117
133
|
radioAnimatedStyle
|
|
118
|
-
]
|
|
134
|
+
]);
|
|
119
135
|
const INNER_CIRCLE_SIZE = 8;
|
|
120
136
|
const circleStyle = (0, react.useMemo)(() => [generated_styles.radioStyles.radioCircle, {
|
|
121
|
-
width: INNER_CIRCLE_SIZE,
|
|
122
|
-
height: INNER_CIRCLE_SIZE,
|
|
137
|
+
width: Math.round(INNER_CIRCLE_SIZE * fontScaleFactor),
|
|
138
|
+
height: Math.round(INNER_CIRCLE_SIZE * fontScaleFactor),
|
|
123
139
|
borderRadius: 9999
|
|
124
|
-
}], [generated_styles.radioStyles.radioCircle]);
|
|
140
|
+
}], [generated_styles.radioStyles.radioCircle, fontScaleFactor]);
|
|
125
141
|
const labelContent = (0, react.useMemo)(() => {
|
|
126
142
|
if (!label) return null;
|
|
127
143
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_FormLabel.FormLabel, {
|
|
@@ -131,12 +147,14 @@ const Radio = (0, react.memo)(function Radio({ label, labelPosition = "start", v
|
|
|
131
147
|
required,
|
|
132
148
|
showRequiredAsterisk: required,
|
|
133
149
|
hasError,
|
|
150
|
+
maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
|
|
134
151
|
style: generated_styles.radioStyles.text
|
|
135
152
|
});
|
|
136
153
|
}, [
|
|
137
154
|
label,
|
|
138
155
|
required,
|
|
139
156
|
hasError,
|
|
157
|
+
resolvedMaxFontSizeMultiplier,
|
|
140
158
|
generated_styles.radioStyles.text
|
|
141
159
|
]);
|
|
142
160
|
const a11yState = (0, react.useMemo)(() => ({
|
|
@@ -14,6 +14,13 @@ interface RadioProps extends Omit<ViewProps, 'style'>, UniversalRadioProps {
|
|
|
14
14
|
disabled?: boolean;
|
|
15
15
|
/** Whether the radio is required */
|
|
16
16
|
required?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Caps how far the circle, dot, and label grow with the OS text-size
|
|
19
|
+
* setting; all three scale by the same factor. Set null to remove the cap;
|
|
20
|
+
* override app-wide via UDSFontScalingProvider.
|
|
21
|
+
* @default 2
|
|
22
|
+
*/
|
|
23
|
+
maxFontSizeMultiplier?: number | null;
|
|
17
24
|
/** Callback fired when the checked state changes */
|
|
18
25
|
onChange?: (checked: boolean) => void;
|
|
19
26
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Radio.d.cts","names":[],"sources":["../../../src/components/Radio/Radio.tsx"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"Radio.d.cts","names":[],"sources":["../../../src/components/Radio/Radio.tsx"],"mappings":";;;;;;;UAsBU,UAAA,SAAmB,IAAA,CAAK,SAAA,YAAqB,mBAAA;;EAErD,GAAA,GAAM,GAAA,CAAI,IAAA;EAFS;EAInB,cAAA;EAJgC;EAMhC,QAAA;EAJM;EAMN,QAAA;EARqD;;;;;;EAerD,qBAAA;EAbM;EAeN,QAAA,IAAY,OAAA;AAAA;;;;;;;;AAAgB;;;;;;;;;;;;;;;;;;;;;;;;;cAmDxB,KAAA,EAAK,OAAA,CAAA,oBAAA,CAAA,UAAA"}
|
|
@@ -14,6 +14,13 @@ interface RadioProps extends Omit<ViewProps, 'style'>, UniversalRadioProps {
|
|
|
14
14
|
disabled?: boolean;
|
|
15
15
|
/** Whether the radio is required */
|
|
16
16
|
required?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Caps how far the circle, dot, and label grow with the OS text-size
|
|
19
|
+
* setting; all three scale by the same factor. Set null to remove the cap;
|
|
20
|
+
* override app-wide via UDSFontScalingProvider.
|
|
21
|
+
* @default 2
|
|
22
|
+
*/
|
|
23
|
+
maxFontSizeMultiplier?: number | null;
|
|
17
24
|
/** Callback fired when the checked state changes */
|
|
18
25
|
onChange?: (checked: boolean) => void;
|
|
19
26
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Radio.d.ts","names":[],"sources":["../../../src/components/Radio/Radio.tsx"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"Radio.d.ts","names":[],"sources":["../../../src/components/Radio/Radio.tsx"],"mappings":";;;;;;;UAsBU,UAAA,SAAmB,IAAA,CAAK,SAAA,YAAqB,mBAAA;;EAErD,GAAA,GAAM,GAAA,CAAI,IAAA;EAFS;EAInB,cAAA;EAJgC;EAMhC,QAAA;EAJM;EAMN,QAAA;EARqD;;;;;;EAerD,qBAAA;EAbM;EAeN,QAAA,IAAY,OAAA;AAAA;;;;;;;;AAAgB;;;;;;;;;;;;;;;;;;;;;;;;;cAmDxB,KAAA,EAAK,OAAA,CAAA,oBAAA,CAAA,UAAA"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*! © 2026 Yahoo, Inc. UDS Mobile v0.0.0-development */
|
|
2
2
|
import { SCALE_EFFECTS } from "../../motion-tokens/dist/index.js";
|
|
3
3
|
import { BUTTON_SPRING_CONFIG } from "../../motion.js";
|
|
4
|
+
import { useMaxFontSizeMultiplier } from "../../fontScaling/useMaxFontSizeMultiplier.js";
|
|
5
|
+
import { useFontScale } from "../../fontScaling/useFontScale.js";
|
|
4
6
|
import { Pressable as Pressable$1 } from "../Pressable.js";
|
|
5
7
|
import { FormLabel } from "../FormLabel.js";
|
|
6
8
|
import { useRadioGroupContext } from "./RadioGroupContext.js";
|
|
@@ -46,9 +48,11 @@ const VARIANT_ERROR_MAP = {
|
|
|
46
48
|
* @see {@link Checkbox} for multi-select options
|
|
47
49
|
* @see {@link Switch} for binary toggles
|
|
48
50
|
*/
|
|
49
|
-
const Radio = memo(function Radio({ label, labelPosition = "start", variant: variantInput, size: sizeInput, value: valueProp, checked: checkedProp, hasError: hasErrorProp, reduceMotion: reduceMotionProp, defaultChecked, disabled: disabledProp, required, onChange, ref, ...viewProps }) {
|
|
51
|
+
const Radio = memo(function Radio({ label, labelPosition = "start", variant: variantInput, size: sizeInput, value: valueProp, checked: checkedProp, hasError: hasErrorProp, reduceMotion: reduceMotionProp, defaultChecked, disabled: disabledProp, required, maxFontSizeMultiplier, onChange, ref, ...viewProps }) {
|
|
50
52
|
const uid = `uds-radio-${useId()}`;
|
|
51
53
|
const radioGroup = useRadioGroupContext();
|
|
54
|
+
const resolvedMaxFontSizeMultiplier = useMaxFontSizeMultiplier("control", maxFontSizeMultiplier) ?? 0;
|
|
55
|
+
const fontScaleFactor = useFontScale(resolvedMaxFontSizeMultiplier);
|
|
52
56
|
const disabled = Boolean(disabledProp || radioGroup?.disabled);
|
|
53
57
|
const hasError = hasErrorProp ?? radioGroup?.hasError;
|
|
54
58
|
const reduceMotion = reduceMotionProp ?? radioGroup?.reduceMotion;
|
|
@@ -104,21 +108,33 @@ const Radio = memo(function Radio({ label, labelPosition = "start", variant: var
|
|
|
104
108
|
pressed
|
|
105
109
|
});
|
|
106
110
|
const rootStyle = useMemo(() => [radioStyles.root, { opacity: disabled ? .5 : 1 }], [radioStyles.root, disabled]);
|
|
107
|
-
const radioBoxStyle = useMemo(() =>
|
|
111
|
+
const radioBoxStyle = useMemo(() => {
|
|
112
|
+
const { width, height } = radioStyles.radio;
|
|
113
|
+
const scaledBox = fontScaleFactor !== 1 && typeof width === "number" && typeof height === "number" ? {
|
|
114
|
+
width: Math.round(width * fontScaleFactor),
|
|
115
|
+
height: Math.round(height * fontScaleFactor)
|
|
116
|
+
} : void 0;
|
|
117
|
+
return [
|
|
118
|
+
radioStyles.radio,
|
|
119
|
+
scaledBox,
|
|
120
|
+
{
|
|
121
|
+
alignItems: "center",
|
|
122
|
+
justifyContent: "center",
|
|
123
|
+
borderRadius: 9999
|
|
124
|
+
},
|
|
125
|
+
radioAnimatedStyle
|
|
126
|
+
];
|
|
127
|
+
}, [
|
|
108
128
|
radioStyles.radio,
|
|
109
|
-
|
|
110
|
-
alignItems: "center",
|
|
111
|
-
justifyContent: "center",
|
|
112
|
-
borderRadius: 9999
|
|
113
|
-
},
|
|
129
|
+
fontScaleFactor,
|
|
114
130
|
radioAnimatedStyle
|
|
115
|
-
]
|
|
131
|
+
]);
|
|
116
132
|
const INNER_CIRCLE_SIZE = 8;
|
|
117
133
|
const circleStyle = useMemo(() => [radioStyles.radioCircle, {
|
|
118
|
-
width: INNER_CIRCLE_SIZE,
|
|
119
|
-
height: INNER_CIRCLE_SIZE,
|
|
134
|
+
width: Math.round(INNER_CIRCLE_SIZE * fontScaleFactor),
|
|
135
|
+
height: Math.round(INNER_CIRCLE_SIZE * fontScaleFactor),
|
|
120
136
|
borderRadius: 9999
|
|
121
|
-
}], [radioStyles.radioCircle]);
|
|
137
|
+
}], [radioStyles.radioCircle, fontScaleFactor]);
|
|
122
138
|
const labelContent = useMemo(() => {
|
|
123
139
|
if (!label) return null;
|
|
124
140
|
return /* @__PURE__ */ jsx(FormLabel, {
|
|
@@ -128,12 +144,14 @@ const Radio = memo(function Radio({ label, labelPosition = "start", variant: var
|
|
|
128
144
|
required,
|
|
129
145
|
showRequiredAsterisk: required,
|
|
130
146
|
hasError,
|
|
147
|
+
maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
|
|
131
148
|
style: radioStyles.text
|
|
132
149
|
});
|
|
133
150
|
}, [
|
|
134
151
|
label,
|
|
135
152
|
required,
|
|
136
153
|
hasError,
|
|
154
|
+
resolvedMaxFontSizeMultiplier,
|
|
137
155
|
radioStyles.text
|
|
138
156
|
]);
|
|
139
157
|
const a11yState = useMemo(() => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Radio.js","names":["Pressable"],"sources":["../../../src/components/Radio/Radio.tsx"],"sourcesContent":["import type { RadioVariant, UniversalRadioProps } from '@yahoo/uds-types';\nimport type { Ref } from 'react';\nimport { memo, useCallback, useEffect, useId, useMemo, useState } from 'react';\nimport type { ViewProps } from 'react-native';\nimport { View } from 'react-native';\nimport Animated, { useAnimatedStyle, useDerivedValue, withSpring } from 'react-native-reanimated';\n\nimport { radioStyles } from '../../../generated/styles';\nimport { BUTTON_SPRING_CONFIG, SCALE_EFFECTS } from '../../motion';\nimport { FormLabel } from '../FormLabel';\nimport { Pressable } from '../Pressable';\nimport { useRadioGroupContext } from './RadioGroupContext';\n\n/* -------------------------------------------------------------------------- */\n/* Types */\n/* -------------------------------------------------------------------------- */\n\n/** Derived from RadioValue - maps boolean to 'checked'/'unchecked' string literals */\ntype RadioValueState = 'checked' | 'unchecked';\n\ninterface RadioProps extends Omit<ViewProps, 'style'>, UniversalRadioProps {\n /** Ref to the underlying View */\n ref?: Ref<View>;\n /** Default checked state for uncontrolled mode */\n defaultChecked?: boolean;\n /** Whether the radio is disabled */\n disabled?: boolean;\n /** Whether the radio is required */\n required?: boolean;\n /** Callback fired when the checked state changes */\n onChange?: (checked: boolean) => void;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Variant Mapping */\n/* -------------------------------------------------------------------------- */\n\n/** All style variants available in the generated styles (includes error variants) */\ntype RadioStyleVariant = 'primary' | 'secondary' | 'alert' | 'alert-secondary';\n\nconst VARIANT_ERROR_MAP: Record<RadioVariant, RadioStyleVariant> = {\n primary: 'alert',\n secondary: 'alert-secondary',\n};\n\n/* -------------------------------------------------------------------------- */\n/* Radio Component */\n/* -------------------------------------------------------------------------- */\n\n/**\n * **⚙️ A radio component**\n *\n * @description\n * A radio component allows users to select one option from a set.\n * Unlike checkbox, radio buttons cannot have multiple selected in a group,\n * allowing only one selection at a time.\n *\n * @category Form\n * @platform mobile\n *\n * @example\n * ```tsx\n * import { Radio } from '@yahoo/uds-mobile/Radio';\n *\n * <Radio label=\"Option A\" />\n * <Radio label=\"Option B\" checked={true} onChange={setChecked} />\n * ```\n *\n * @usage\n * - Forms: For selecting a single option from multiple choices\n * - Settings: For toggling between mutually exclusive preferences\n *\n * @accessibility\n * - Sets `accessibilityRole=\"radio\"` automatically\n * - Announces checked/unchecked state\n * - Label is used as accessibility label\n * - Supports `reduceMotion` for users who prefer reduced motion\n *\n * @see {@link Checkbox} for multi-select options\n * @see {@link Switch} for binary toggles\n */\nconst Radio = memo(function Radio({\n // Radio props\n label,\n labelPosition = 'start',\n variant: variantInput,\n size: sizeInput,\n value: valueProp,\n checked: checkedProp,\n hasError: hasErrorProp,\n reduceMotion: reduceMotionProp,\n // Native props\n defaultChecked,\n disabled: disabledProp,\n required,\n onChange,\n ref,\n ...viewProps\n}: RadioProps) {\n const generatedId = useId();\n const uid = `uds-radio-${generatedId}`;\n const radioGroup = useRadioGroupContext();\n\n const disabled = Boolean(disabledProp || radioGroup?.disabled);\n const hasError = hasErrorProp ?? radioGroup?.hasError;\n const reduceMotion = reduceMotionProp ?? radioGroup?.reduceMotion;\n const size = sizeInput ?? radioGroup?.size ?? 'md';\n const variantProp = variantInput ?? radioGroup?.variant ?? 'primary';\n const value = valueProp ?? '';\n\n /* --------------------------------- State ---------------------------------- */\n const isInRadioGroup = radioGroup !== undefined;\n const isControlled = checkedProp !== undefined;\n const [internalChecked, setInternalChecked] = useState<boolean>(\n isControlled ? checkedProp : (defaultChecked ?? false),\n );\n const [pressed, setPressed] = useState(false);\n\n // Use controlled value if provided, otherwise use internal state\n const checked = isInRadioGroup\n ? radioGroup.value === value\n : isControlled\n ? checkedProp\n : internalChecked;\n\n useEffect(() => {\n if (!__DEV__ || !isInRadioGroup || valueProp) {\n return;\n }\n\n console.warn(\n '[UDS Mobile] Radio: A Radio rendered inside RadioGroup requires a unique, non-empty value prop.',\n );\n }, [isInRadioGroup, valueProp]);\n\n /* ------------------------------ Derived Values ---------------------------- */\n const valueState: RadioValueState = useMemo(() => {\n return checked ? 'checked' : 'unchecked';\n }, [checked]);\n\n /* -------------------------------- Animation ------------------------------- */\n const shouldAnimate = !reduceMotion;\n\n // Scale animation for press feedback (disabled when reduceMotion is true)\n const scaleProgress = useDerivedValue(\n () =>\n shouldAnimate\n ? withSpring(pressed ? SCALE_EFFECTS.down : SCALE_EFFECTS.none, BUTTON_SPRING_CONFIG)\n : SCALE_EFFECTS.none, // No scale effect when reduceMotion is true\n [pressed, shouldAnimate],\n );\n\n const radioAnimatedStyle = useAnimatedStyle(() => ({\n transform: [{ scale: scaleProgress.value }],\n }));\n\n /* -------------------------------- Handlers -------------------------------- */\n const handlePress = useCallback(() => {\n if (disabled) {\n return;\n }\n\n if (isInRadioGroup) {\n if (radioGroup.value === value) {\n return;\n }\n\n radioGroup.setValue(value);\n onChange?.(true);\n return;\n }\n\n const newChecked = !checked;\n\n if (!isControlled) {\n setInternalChecked(newChecked);\n }\n\n onChange?.(newChecked);\n }, [disabled, isInRadioGroup, checked, isControlled, onChange, radioGroup, value]);\n\n const handlePressIn = useCallback(() => {\n setPressed(true);\n }, []);\n\n const handlePressOut = useCallback(() => {\n setPressed(false);\n }, []);\n\n /* --------------------------------- Styles --------------------------------- */\n // Compute the style variant (primary/secondary maps to alert variants when hasError)\n const styleVariant: RadioStyleVariant = hasError ? VARIANT_ERROR_MAP[variantProp] : variantProp;\n\n radioStyles.useVariants({\n size,\n variant: styleVariant, // primary, secondary, alert, alert-secondary\n value: valueState, // checked, unchecked\n pressed,\n });\n\n const rootStyle = useMemo(\n () => [radioStyles.root, { opacity: disabled ? 0.5 : 1 }],\n [radioStyles.root, disabled],\n );\n\n const radioBoxStyle = useMemo(\n () => [\n radioStyles.radio,\n {\n alignItems: 'center' as const,\n justifyContent: 'center' as const,\n borderRadius: 9999,\n },\n radioAnimatedStyle,\n ],\n [radioStyles.radio, radioAnimatedStyle],\n );\n\n // Inner circle size: fixed 8x8px to match web implementation\n const INNER_CIRCLE_SIZE = 8;\n\n const circleStyle = useMemo(\n () => [\n radioStyles.radioCircle,\n {\n width: INNER_CIRCLE_SIZE,\n height: INNER_CIRCLE_SIZE,\n borderRadius: 9999,\n },\n ],\n [radioStyles.radioCircle],\n );\n\n /* ---------------------------- Render Helpers ------------------------------ */\n const labelContent = useMemo(() => {\n if (!label) {\n return null;\n }\n\n return (\n <FormLabel\n color=\"inherit\"\n variant=\"inherit\"\n label={label}\n required={required}\n showRequiredAsterisk={required}\n hasError={hasError}\n style={radioStyles.text}\n />\n );\n }, [label, required, hasError, radioStyles.text]);\n\n const a11yState = useMemo(\n () => ({\n disabled,\n checked,\n }),\n [disabled, checked],\n );\n\n /* --------------------------------- Render --------------------------------- */\n return (\n <Pressable\n ref={ref}\n nativeID={uid}\n onPress={handlePress}\n onPressIn={handlePressIn}\n onPressOut={handlePressOut}\n disabled={disabled}\n flexDirection={labelPosition === 'start' ? 'row' : 'row-reverse'}\n alignItems=\"center\"\n alignSelf=\"flex-start\"\n accessibilityRole=\"radio\"\n accessibilityState={a11yState}\n accessibilityLabel={typeof label === 'string' ? label : undefined}\n {...viewProps}\n style={rootStyle}\n >\n <Animated.View style={radioBoxStyle}>{checked && <View style={circleStyle} />}</Animated.View>\n\n {labelContent}\n </Pressable>\n );\n});\n\nRadio.displayName = 'Radio';\n\nexport { Radio, type RadioProps };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Radio.js","names":["Pressable"],"sources":["../../../src/components/Radio/Radio.tsx"],"sourcesContent":["import type { RadioVariant, UniversalRadioProps } from '@yahoo/uds-types';\nimport type { Ref } from 'react';\nimport { memo, useCallback, useEffect, useId, useMemo, useState } from 'react';\nimport type { ViewProps } from 'react-native';\nimport { View } from 'react-native';\nimport Animated, { useAnimatedStyle, useDerivedValue, withSpring } from 'react-native-reanimated';\n\nimport { radioStyles } from '../../../generated/styles';\nimport { useFontScale } from '../../fontScaling/useFontScale';\nimport { useMaxFontSizeMultiplier } from '../../fontScaling/useMaxFontSizeMultiplier';\nimport { BUTTON_SPRING_CONFIG, SCALE_EFFECTS } from '../../motion';\nimport { FormLabel } from '../FormLabel';\nimport { Pressable } from '../Pressable';\nimport { useRadioGroupContext } from './RadioGroupContext';\n\n/* -------------------------------------------------------------------------- */\n/* Types */\n/* -------------------------------------------------------------------------- */\n\n/** Derived from RadioValue - maps boolean to 'checked'/'unchecked' string literals */\ntype RadioValueState = 'checked' | 'unchecked';\n\ninterface RadioProps extends Omit<ViewProps, 'style'>, UniversalRadioProps {\n /** Ref to the underlying View */\n ref?: Ref<View>;\n /** Default checked state for uncontrolled mode */\n defaultChecked?: boolean;\n /** Whether the radio is disabled */\n disabled?: boolean;\n /** Whether the radio is required */\n required?: boolean;\n /**\n * Caps how far the circle, dot, and label grow with the OS text-size\n * setting; all three scale by the same factor. Set null to remove the cap;\n * override app-wide via UDSFontScalingProvider.\n * @default 2\n */\n maxFontSizeMultiplier?: number | null;\n /** Callback fired when the checked state changes */\n onChange?: (checked: boolean) => void;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Variant Mapping */\n/* -------------------------------------------------------------------------- */\n\n/** All style variants available in the generated styles (includes error variants) */\ntype RadioStyleVariant = 'primary' | 'secondary' | 'alert' | 'alert-secondary';\n\nconst VARIANT_ERROR_MAP: Record<RadioVariant, RadioStyleVariant> = {\n primary: 'alert',\n secondary: 'alert-secondary',\n};\n\n/* -------------------------------------------------------------------------- */\n/* Radio Component */\n/* -------------------------------------------------------------------------- */\n\n/**\n * **⚙️ A radio component**\n *\n * @description\n * A radio component allows users to select one option from a set.\n * Unlike checkbox, radio buttons cannot have multiple selected in a group,\n * allowing only one selection at a time.\n *\n * @category Form\n * @platform mobile\n *\n * @example\n * ```tsx\n * import { Radio } from '@yahoo/uds-mobile/Radio';\n *\n * <Radio label=\"Option A\" />\n * <Radio label=\"Option B\" checked={true} onChange={setChecked} />\n * ```\n *\n * @usage\n * - Forms: For selecting a single option from multiple choices\n * - Settings: For toggling between mutually exclusive preferences\n *\n * @accessibility\n * - Sets `accessibilityRole=\"radio\"` automatically\n * - Announces checked/unchecked state\n * - Label is used as accessibility label\n * - Supports `reduceMotion` for users who prefer reduced motion\n *\n * @see {@link Checkbox} for multi-select options\n * @see {@link Switch} for binary toggles\n */\nconst Radio = memo(function Radio({\n // Radio props\n label,\n labelPosition = 'start',\n variant: variantInput,\n size: sizeInput,\n value: valueProp,\n checked: checkedProp,\n hasError: hasErrorProp,\n reduceMotion: reduceMotionProp,\n // Native props\n defaultChecked,\n disabled: disabledProp,\n required,\n maxFontSizeMultiplier,\n onChange,\n ref,\n ...viewProps\n}: RadioProps) {\n const generatedId = useId();\n const uid = `uds-radio-${generatedId}`;\n const radioGroup = useRadioGroupContext();\n\n // One capped factor governs circle, dot, and label so they stay cohesive.\n const resolvedMaxFontSizeMultiplier =\n useMaxFontSizeMultiplier('control', maxFontSizeMultiplier) ?? 0;\n const fontScaleFactor = useFontScale(resolvedMaxFontSizeMultiplier);\n\n const disabled = Boolean(disabledProp || radioGroup?.disabled);\n const hasError = hasErrorProp ?? radioGroup?.hasError;\n const reduceMotion = reduceMotionProp ?? radioGroup?.reduceMotion;\n const size = sizeInput ?? radioGroup?.size ?? 'md';\n const variantProp = variantInput ?? radioGroup?.variant ?? 'primary';\n const value = valueProp ?? '';\n\n /* --------------------------------- State ---------------------------------- */\n const isInRadioGroup = radioGroup !== undefined;\n const isControlled = checkedProp !== undefined;\n const [internalChecked, setInternalChecked] = useState<boolean>(\n isControlled ? checkedProp : (defaultChecked ?? false),\n );\n const [pressed, setPressed] = useState(false);\n\n // Use controlled value if provided, otherwise use internal state\n const checked = isInRadioGroup\n ? radioGroup.value === value\n : isControlled\n ? checkedProp\n : internalChecked;\n\n useEffect(() => {\n if (!__DEV__ || !isInRadioGroup || valueProp) {\n return;\n }\n\n console.warn(\n '[UDS Mobile] Radio: A Radio rendered inside RadioGroup requires a unique, non-empty value prop.',\n );\n }, [isInRadioGroup, valueProp]);\n\n /* ------------------------------ Derived Values ---------------------------- */\n const valueState: RadioValueState = useMemo(() => {\n return checked ? 'checked' : 'unchecked';\n }, [checked]);\n\n /* -------------------------------- Animation ------------------------------- */\n const shouldAnimate = !reduceMotion;\n\n // Scale animation for press feedback (disabled when reduceMotion is true)\n const scaleProgress = useDerivedValue(\n () =>\n shouldAnimate\n ? withSpring(pressed ? SCALE_EFFECTS.down : SCALE_EFFECTS.none, BUTTON_SPRING_CONFIG)\n : SCALE_EFFECTS.none, // No scale effect when reduceMotion is true\n [pressed, shouldAnimate],\n );\n\n const radioAnimatedStyle = useAnimatedStyle(() => ({\n transform: [{ scale: scaleProgress.value }],\n }));\n\n /* -------------------------------- Handlers -------------------------------- */\n const handlePress = useCallback(() => {\n if (disabled) {\n return;\n }\n\n if (isInRadioGroup) {\n if (radioGroup.value === value) {\n return;\n }\n\n radioGroup.setValue(value);\n onChange?.(true);\n return;\n }\n\n const newChecked = !checked;\n\n if (!isControlled) {\n setInternalChecked(newChecked);\n }\n\n onChange?.(newChecked);\n }, [disabled, isInRadioGroup, checked, isControlled, onChange, radioGroup, value]);\n\n const handlePressIn = useCallback(() => {\n setPressed(true);\n }, []);\n\n const handlePressOut = useCallback(() => {\n setPressed(false);\n }, []);\n\n /* --------------------------------- Styles --------------------------------- */\n // Compute the style variant (primary/secondary maps to alert variants when hasError)\n const styleVariant: RadioStyleVariant = hasError ? VARIANT_ERROR_MAP[variantProp] : variantProp;\n\n radioStyles.useVariants({\n size,\n variant: styleVariant, // primary, secondary, alert, alert-secondary\n value: valueState, // checked, unchecked\n pressed,\n });\n\n const rootStyle = useMemo(\n () => [radioStyles.root, { opacity: disabled ? 0.5 : 1 }],\n [radioStyles.root, disabled],\n );\n\n const radioBoxStyle = useMemo(() => {\n // The circle grows by the capped factor alongside its label; borderWidth\n // stays fixed (stroke weight doesn't scale in the OS's own controls).\n const { width, height } = radioStyles.radio;\n const scaledBox =\n fontScaleFactor !== 1 && typeof width === 'number' && typeof height === 'number'\n ? {\n width: Math.round(width * fontScaleFactor),\n height: Math.round(height * fontScaleFactor),\n }\n : undefined;\n\n return [\n radioStyles.radio,\n scaledBox,\n {\n alignItems: 'center' as const,\n justifyContent: 'center' as const,\n borderRadius: 9999,\n },\n radioAnimatedStyle,\n ];\n }, [radioStyles.radio, fontScaleFactor, radioAnimatedStyle]);\n\n // Inner circle size: fixed 8x8px to match web implementation\n const INNER_CIRCLE_SIZE = 8;\n\n const circleStyle = useMemo(\n () => [\n radioStyles.radioCircle,\n {\n width: Math.round(INNER_CIRCLE_SIZE * fontScaleFactor),\n height: Math.round(INNER_CIRCLE_SIZE * fontScaleFactor),\n borderRadius: 9999,\n },\n ],\n [radioStyles.radioCircle, fontScaleFactor],\n );\n\n /* ---------------------------- Render Helpers ------------------------------ */\n const labelContent = useMemo(() => {\n if (!label) {\n return null;\n }\n\n return (\n <FormLabel\n color=\"inherit\"\n variant=\"inherit\"\n label={label}\n required={required}\n showRequiredAsterisk={required}\n hasError={hasError}\n maxFontSizeMultiplier={resolvedMaxFontSizeMultiplier}\n style={radioStyles.text}\n />\n );\n }, [label, required, hasError, resolvedMaxFontSizeMultiplier, radioStyles.text]);\n\n const a11yState = useMemo(\n () => ({\n disabled,\n checked,\n }),\n [disabled, checked],\n );\n\n /* --------------------------------- Render --------------------------------- */\n return (\n <Pressable\n ref={ref}\n nativeID={uid}\n onPress={handlePress}\n onPressIn={handlePressIn}\n onPressOut={handlePressOut}\n disabled={disabled}\n flexDirection={labelPosition === 'start' ? 'row' : 'row-reverse'}\n alignItems=\"center\"\n alignSelf=\"flex-start\"\n accessibilityRole=\"radio\"\n accessibilityState={a11yState}\n accessibilityLabel={typeof label === 'string' ? label : undefined}\n {...viewProps}\n style={rootStyle}\n >\n <Animated.View style={radioBoxStyle}>{checked && <View style={circleStyle} />}</Animated.View>\n\n {labelContent}\n </Pressable>\n );\n});\n\nRadio.displayName = 'Radio';\n\nexport { Radio, type RadioProps };\n"],"mappings":";;;;;;;;;;;;;;AAiDA,MAAM,oBAA6D;CACjE,SAAS;CACT,WAAW;CACZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCD,MAAM,QAAQ,KAAK,SAAS,MAAM,EAEhC,OACA,gBAAgB,SAChB,SAAS,cACT,MAAM,WACN,OAAO,WACP,SAAS,aACT,UAAU,cACV,cAAc,kBAEd,gBACA,UAAU,cACV,UACA,uBACA,UACA,KACA,GAAG,aACU;CAEb,MAAM,MAAM,aADQ,OACgB;CACpC,MAAM,aAAa,sBAAsB;CAGzC,MAAM,gCACJ,yBAAyB,WAAW,sBAAsB,IAAI;CAChE,MAAM,kBAAkB,aAAa,8BAA8B;CAEnE,MAAM,WAAW,QAAQ,gBAAgB,YAAY,SAAS;CAC9D,MAAM,WAAW,gBAAgB,YAAY;CAC7C,MAAM,eAAe,oBAAoB,YAAY;CACrD,MAAM,OAAO,aAAa,YAAY,QAAQ;CAC9C,MAAM,cAAc,gBAAgB,YAAY,WAAW;CAC3D,MAAM,QAAQ,aAAa;CAG3B,MAAM,iBAAiB,eAAe,KAAA;CACtC,MAAM,eAAe,gBAAgB,KAAA;CACrC,MAAM,CAAC,iBAAiB,sBAAsB,SAC5C,eAAe,cAAe,kBAAkB,MACjD;CACD,MAAM,CAAC,SAAS,cAAc,SAAS,MAAM;CAG7C,MAAM,UAAU,iBACZ,WAAW,UAAU,QACrB,eACE,cACA;CAEN,gBAAgB;EACd,IAAI,CAAC,WAAW,CAAC,kBAAkB,WACjC;EAGF,QAAQ,KACN,kGACD;IACA,CAAC,gBAAgB,UAAU,CAAC;CAG/B,MAAM,aAA8B,cAAc;EAChD,OAAO,UAAU,YAAY;IAC5B,CAAC,QAAQ,CAAC;CAGb,MAAM,gBAAgB,CAAC;CAGvB,MAAM,gBAAgB,sBAElB,gBACI,WAAW,UAAU,cAAc,OAAO,cAAc,MAAM,qBAAqB,GACnF,cAAc,MACpB,CAAC,SAAS,cAAc,CACzB;CAED,MAAM,qBAAqB,wBAAwB,EACjD,WAAW,CAAC,EAAE,OAAO,cAAc,OAAO,CAAC,EAC5C,EAAE;CAGH,MAAM,cAAc,kBAAkB;EACpC,IAAI,UACF;EAGF,IAAI,gBAAgB;GAClB,IAAI,WAAW,UAAU,OACvB;GAGF,WAAW,SAAS,MAAM;GAC1B,WAAW,KAAK;GAChB;;EAGF,MAAM,aAAa,CAAC;EAEpB,IAAI,CAAC,cACH,mBAAmB,WAAW;EAGhC,WAAW,WAAW;IACrB;EAAC;EAAU;EAAgB;EAAS;EAAc;EAAU;EAAY;EAAM,CAAC;CAElF,MAAM,gBAAgB,kBAAkB;EACtC,WAAW,KAAK;IACf,EAAE,CAAC;CAEN,MAAM,iBAAiB,kBAAkB;EACvC,WAAW,MAAM;IAChB,EAAE,CAAC;CAIN,MAAM,eAAkC,WAAW,kBAAkB,eAAe;CAEpF,YAAY,YAAY;EACtB;EACA,SAAS;EACT,OAAO;EACP;EACD,CAAC;CAEF,MAAM,YAAY,cACV,CAAC,YAAY,MAAM,EAAE,SAAS,WAAW,KAAM,GAAG,CAAC,EACzD,CAAC,YAAY,MAAM,SAAS,CAC7B;CAED,MAAM,gBAAgB,cAAc;EAGlC,MAAM,EAAE,OAAO,WAAW,YAAY;EACtC,MAAM,YACJ,oBAAoB,KAAK,OAAO,UAAU,YAAY,OAAO,WAAW,WACpE;GACE,OAAO,KAAK,MAAM,QAAQ,gBAAgB;GAC1C,QAAQ,KAAK,MAAM,SAAS,gBAAgB;GAC7C,GACD,KAAA;EAEN,OAAO;GACL,YAAY;GACZ;GACA;IACE,YAAY;IACZ,gBAAgB;IAChB,cAAc;IACf;GACD;GACD;IACA;EAAC,YAAY;EAAO;EAAiB;EAAmB,CAAC;CAG5D,MAAM,oBAAoB;CAE1B,MAAM,cAAc,cACZ,CACJ,YAAY,aACZ;EACE,OAAO,KAAK,MAAM,oBAAoB,gBAAgB;EACtD,QAAQ,KAAK,MAAM,oBAAoB,gBAAgB;EACvD,cAAc;EACf,CACF,EACD,CAAC,YAAY,aAAa,gBAAgB,CAC3C;CAGD,MAAM,eAAe,cAAc;EACjC,IAAI,CAAC,OACH,OAAO;EAGT,OACE,oBAAC,WAAD;GACE,OAAM;GACN,SAAQ;GACD;GACG;GACV,sBAAsB;GACZ;GACV,uBAAuB;GACvB,OAAO,YAAY;GACnB,CAAA;IAEH;EAAC;EAAO;EAAU;EAAU;EAA+B,YAAY;EAAK,CAAC;CAEhF,MAAM,YAAY,eACT;EACL;EACA;EACD,GACD,CAAC,UAAU,QAAQ,CACpB;CAGD,OACE,qBAACA,aAAD;EACO;EACL,UAAU;EACV,SAAS;EACT,WAAW;EACX,YAAY;EACF;EACV,eAAe,kBAAkB,UAAU,QAAQ;EACnD,YAAW;EACX,WAAU;EACV,mBAAkB;EAClB,oBAAoB;EACpB,oBAAoB,OAAO,UAAU,WAAW,QAAQ,KAAA;EACxD,GAAI;EACJ,OAAO;YAdT,CAgBE,oBAAC,SAAS,MAAV;GAAe,OAAO;aAAgB,WAAW,oBAAC,MAAD,EAAM,OAAO,aAAe,CAAA;GAAiB,CAAA,EAE7F,aACS;;EAEd;AAEF,MAAM,cAAc"}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/*! © 2026 Yahoo, Inc. UDS Mobile v0.0.0-development */
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
4
|
+
const require_fontScaling_useMaxFontSizeMultiplier = require("../fontScaling/useMaxFontSizeMultiplier.cjs");
|
|
4
5
|
const require_components_IconSlot = require("./IconSlot.cjs");
|
|
6
|
+
const require_fontScaling_useFontScale = require("../fontScaling/useFontScale.cjs");
|
|
5
7
|
const require_components_FormLabel = require("./FormLabel.cjs");
|
|
6
8
|
let react = require("react");
|
|
7
9
|
let react_native = require("react-native");
|
|
8
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
11
|
let generated_styles = require("../../generated/styles");
|
|
10
12
|
let react_native_unistyles = require("react-native-unistyles");
|
|
13
|
+
let _yahoo_uds_icons_tokens = require("@yahoo/uds-icons/tokens");
|
|
11
14
|
let react_native_reanimated = require("react-native-reanimated");
|
|
12
15
|
react_native_reanimated = require_runtime.__toESM(react_native_reanimated);
|
|
13
16
|
let react_native_unistyles_reanimated = require("react-native-unistyles/reanimated");
|
|
@@ -50,13 +53,16 @@ const ANIMATION_DURATION = 120;
|
|
|
50
53
|
* @see {@link Checkbox} for forms with submit actions
|
|
51
54
|
* @see {@link Radio} for single-select options
|
|
52
55
|
*/
|
|
53
|
-
const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = false, onChange, label, labelPosition = "start", size = "md", onIcon, offIcon, disabled = false, required, accessibilityHint, reduceMotion = false, ref, ...viewProps }) {
|
|
56
|
+
const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = false, onChange, label, labelPosition = "start", size = "md", onIcon, offIcon, disabled = false, required, maxFontSizeMultiplier, accessibilityHint, reduceMotion = false, ref, ...viewProps }) {
|
|
54
57
|
const isControlled = isOnProp !== void 0;
|
|
55
58
|
const [internalIsOn, setInternalIsOn] = (0, react.useState)(defaultIsOn);
|
|
56
59
|
const [prefersReducedMotion, setPrefersReducedMotion] = (0, react.useState)(false);
|
|
57
60
|
const isOn = isControlled ? isOnProp : internalIsOn;
|
|
58
61
|
const activeVariant = isOn ? "on" : "off";
|
|
59
62
|
const { theme } = (0, react_native_unistyles.useUnistyles)();
|
|
63
|
+
const resolvedMaxFontSizeMultiplier = require_fontScaling_useMaxFontSizeMultiplier.useMaxFontSizeMultiplier("control", maxFontSizeMultiplier) ?? 0;
|
|
64
|
+
const fontScaleFactor = require_fontScaling_useFontScale.useFontScale(resolvedMaxFontSizeMultiplier);
|
|
65
|
+
const scaledHandleIconSize = Math.round(_yahoo_uds_icons_tokens.ICON_SIZE_MAP.sm * fontScaleFactor);
|
|
60
66
|
(0, react.useEffect)(() => {
|
|
61
67
|
const checkReducedMotion = async () => {
|
|
62
68
|
setPrefersReducedMotion(await react_native.AccessibilityInfo.isReduceMotionEnabled());
|
|
@@ -67,7 +73,7 @@ const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = f
|
|
|
67
73
|
}, []);
|
|
68
74
|
const animationDuration = reduceMotion || prefersReducedMotion ? 0 : ANIMATION_DURATION;
|
|
69
75
|
const progress = (0, react_native_reanimated.useDerivedValue)(() => (0, react_native_reanimated.withTiming)(isOn ? 1 : 0, { duration: animationDuration }), [isOn, animationDuration]);
|
|
70
|
-
const travelDistance = HANDLE_TRAVEL[size] * (react_native.I18nManager.isRTL ? -1 : 1);
|
|
76
|
+
const travelDistance = Math.round(HANDLE_TRAVEL[size] * fontScaleFactor) * (react_native.I18nManager.isRTL ? -1 : 1);
|
|
71
77
|
const handlePress = (0, react.useCallback)(() => {
|
|
72
78
|
if (disabled) return;
|
|
73
79
|
const newValue = !isOn;
|
|
@@ -103,24 +109,36 @@ const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = f
|
|
|
103
109
|
return { transform: [{ translateX: progress.value * travelDistance }] };
|
|
104
110
|
});
|
|
105
111
|
const rootStyle = (0, react.useMemo)(() => [resolvedSwitchStyles.root, switchStaticStyles.root({ disabled })], [resolvedSwitchStyles.root, disabled]);
|
|
112
|
+
const scaleDimensions = (0, react.useCallback)((style) => {
|
|
113
|
+
if (!style || fontScaleFactor === 1) return;
|
|
114
|
+
const { width, height } = style;
|
|
115
|
+
return typeof width === "number" && typeof height === "number" ? {
|
|
116
|
+
width: Math.round(width * fontScaleFactor),
|
|
117
|
+
height: Math.round(height * fontScaleFactor)
|
|
118
|
+
} : void 0;
|
|
119
|
+
}, [fontScaleFactor]);
|
|
106
120
|
const trackStyle = (0, react.useMemo)(() => [
|
|
107
121
|
resolvedSwitchStyles.switch,
|
|
108
122
|
switchStaticStyles.track,
|
|
109
123
|
variantLayerStyles.switch,
|
|
124
|
+
scaleDimensions(resolvedSwitchStyles.switch),
|
|
110
125
|
react_native.Platform.OS !== "web" && animatedTrackStyle
|
|
111
126
|
], [
|
|
112
127
|
resolvedSwitchStyles.switch,
|
|
113
128
|
variantLayerStyles.switch,
|
|
129
|
+
scaleDimensions,
|
|
114
130
|
animatedTrackStyle
|
|
115
131
|
]);
|
|
116
132
|
const handleStyle = (0, react.useMemo)(() => [
|
|
117
133
|
resolvedSwitchStyles.handle,
|
|
118
134
|
switchStaticStyles.handle,
|
|
119
135
|
variantLayerStyles.handle,
|
|
136
|
+
scaleDimensions(resolvedSwitchStyles.handle),
|
|
120
137
|
animatedHandleStyle
|
|
121
138
|
], [
|
|
122
139
|
resolvedSwitchStyles.handle,
|
|
123
140
|
variantLayerStyles.handle,
|
|
141
|
+
scaleDimensions,
|
|
124
142
|
animatedHandleStyle
|
|
125
143
|
]);
|
|
126
144
|
const accessibilityLabel = typeof label === "string" ? label : void 0;
|
|
@@ -131,6 +149,7 @@ const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = f
|
|
|
131
149
|
label,
|
|
132
150
|
required,
|
|
133
151
|
showRequiredAsterisk: required,
|
|
152
|
+
maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
|
|
134
153
|
style: [resolvedSwitchStyles.text, variantLayerStyles.text]
|
|
135
154
|
});
|
|
136
155
|
const a11yValue = (0, react.useMemo)(() => ({ text: isOn ? "On" : "Off" }), [isOn]);
|
|
@@ -161,6 +180,8 @@ const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = f
|
|
|
161
180
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_IconSlot.IconSlot, {
|
|
162
181
|
icon: onIcon,
|
|
163
182
|
variant: "fill",
|
|
183
|
+
dangerouslySetSize: scaledHandleIconSize,
|
|
184
|
+
allowFontScaling: false,
|
|
164
185
|
style: [resolvedSwitchStyles.handleIcon, variantLayerStyles.handleIcon]
|
|
165
186
|
})
|
|
166
187
|
}), offIcon && !isOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
|
|
@@ -168,6 +189,8 @@ const Switch = (0, react.memo)(function Switch({ isOn: isOnProp, defaultIsOn = f
|
|
|
168
189
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_IconSlot.IconSlot, {
|
|
169
190
|
icon: offIcon,
|
|
170
191
|
variant: "fill",
|
|
192
|
+
dangerouslySetSize: scaledHandleIconSize,
|
|
193
|
+
allowFontScaling: false,
|
|
171
194
|
style: [resolvedSwitchStyles.handleIcon, variantLayerStyles.handleIcon]
|
|
172
195
|
})
|
|
173
196
|
})]
|
|
@@ -15,6 +15,13 @@ interface SwitchProps extends Omit<ViewProps, 'style'>, UniversalSwitchProps<Ico
|
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
/** Whether the switch is required (shows asterisk with label) */
|
|
17
17
|
required?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Caps how far the track, handle, and label grow with the OS text-size
|
|
20
|
+
* setting; all scale by the same factor. Set null to remove the cap;
|
|
21
|
+
* override app-wide via UDSFontScalingProvider.
|
|
22
|
+
* @default 2
|
|
23
|
+
*/
|
|
24
|
+
maxFontSizeMultiplier?: number | null;
|
|
18
25
|
/** Accessibility hint describing what happens when activated */
|
|
19
26
|
accessibilityHint?: AccessibilityProps['accessibilityHint'];
|
|
20
27
|
}
|
|
@@ -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":";;;;;;;;UAyBU,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;;;;;;EAaN,qBAAA;EAfiC;EAiBjC,iBAAA,GAAoB,kBAAA;AAAA;;;;;;;;;;;;;AAAkB;;;;;;;;;;;;;;;;;;;;;cA2ClC,MAAA,EAAM,OAAA,CAAA,oBAAA,CAAA,WAAA"}
|
|
@@ -15,6 +15,13 @@ interface SwitchProps extends Omit<ViewProps, 'style'>, UniversalSwitchProps<Ico
|
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
/** Whether the switch is required (shows asterisk with label) */
|
|
17
17
|
required?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Caps how far the track, handle, and label grow with the OS text-size
|
|
20
|
+
* setting; all scale by the same factor. Set null to remove the cap;
|
|
21
|
+
* override app-wide via UDSFontScalingProvider.
|
|
22
|
+
* @default 2
|
|
23
|
+
*/
|
|
24
|
+
maxFontSizeMultiplier?: number | null;
|
|
18
25
|
/** Accessibility hint describing what happens when activated */
|
|
19
26
|
accessibilityHint?: AccessibilityProps['accessibilityHint'];
|
|
20
27
|
}
|
|
@@ -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":";;;;;;;;UAyBU,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;;;;;;EAaN,qBAAA;EAfiC;EAiBjC,iBAAA,GAAoB,kBAAA;AAAA;;;;;;;;;;;;;AAAkB;;;;;;;;;;;;;;;;;;;;;cA2ClC,MAAA,EAAM,OAAA,CAAA,oBAAA,CAAA,WAAA"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/*! © 2026 Yahoo, Inc. UDS Mobile v0.0.0-development */
|
|
2
|
+
import { useMaxFontSizeMultiplier } from "../fontScaling/useMaxFontSizeMultiplier.js";
|
|
2
3
|
import { IconSlot } from "./IconSlot.js";
|
|
4
|
+
import { useFontScale } from "../fontScaling/useFontScale.js";
|
|
3
5
|
import { FormLabel } from "./FormLabel.js";
|
|
4
6
|
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
5
7
|
import { AccessibilityInfo, I18nManager, Platform, Pressable } from "react-native";
|
|
6
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
9
|
import { switchStyles } from "../../generated/styles";
|
|
8
10
|
import { StyleSheet as StyleSheet$1, useUnistyles } from "react-native-unistyles";
|
|
11
|
+
import { ICON_SIZE_MAP } from "@yahoo/uds-icons/tokens";
|
|
9
12
|
import Animated, { useAnimatedStyle, useDerivedValue, withTiming } from "react-native-reanimated";
|
|
10
13
|
import { useAnimatedVariantColor } from "react-native-unistyles/reanimated";
|
|
11
14
|
//#region src/components/Switch.tsx
|
|
@@ -47,13 +50,16 @@ const ANIMATION_DURATION = 120;
|
|
|
47
50
|
* @see {@link Checkbox} for forms with submit actions
|
|
48
51
|
* @see {@link Radio} for single-select options
|
|
49
52
|
*/
|
|
50
|
-
const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onChange, label, labelPosition = "start", size = "md", onIcon, offIcon, disabled = false, required, accessibilityHint, reduceMotion = false, ref, ...viewProps }) {
|
|
53
|
+
const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onChange, label, labelPosition = "start", size = "md", onIcon, offIcon, disabled = false, required, maxFontSizeMultiplier, accessibilityHint, reduceMotion = false, ref, ...viewProps }) {
|
|
51
54
|
const isControlled = isOnProp !== void 0;
|
|
52
55
|
const [internalIsOn, setInternalIsOn] = useState(defaultIsOn);
|
|
53
56
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
|
54
57
|
const isOn = isControlled ? isOnProp : internalIsOn;
|
|
55
58
|
const activeVariant = isOn ? "on" : "off";
|
|
56
59
|
const { theme } = useUnistyles();
|
|
60
|
+
const resolvedMaxFontSizeMultiplier = useMaxFontSizeMultiplier("control", maxFontSizeMultiplier) ?? 0;
|
|
61
|
+
const fontScaleFactor = useFontScale(resolvedMaxFontSizeMultiplier);
|
|
62
|
+
const scaledHandleIconSize = Math.round(ICON_SIZE_MAP.sm * fontScaleFactor);
|
|
57
63
|
useEffect(() => {
|
|
58
64
|
const checkReducedMotion = async () => {
|
|
59
65
|
setPrefersReducedMotion(await AccessibilityInfo.isReduceMotionEnabled());
|
|
@@ -64,7 +70,7 @@ const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onCha
|
|
|
64
70
|
}, []);
|
|
65
71
|
const animationDuration = reduceMotion || prefersReducedMotion ? 0 : ANIMATION_DURATION;
|
|
66
72
|
const progress = useDerivedValue(() => withTiming(isOn ? 1 : 0, { duration: animationDuration }), [isOn, animationDuration]);
|
|
67
|
-
const travelDistance = HANDLE_TRAVEL[size] * (I18nManager.isRTL ? -1 : 1);
|
|
73
|
+
const travelDistance = Math.round(HANDLE_TRAVEL[size] * fontScaleFactor) * (I18nManager.isRTL ? -1 : 1);
|
|
68
74
|
const handlePress = useCallback(() => {
|
|
69
75
|
if (disabled) return;
|
|
70
76
|
const newValue = !isOn;
|
|
@@ -100,24 +106,36 @@ const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onCha
|
|
|
100
106
|
return { transform: [{ translateX: progress.value * travelDistance }] };
|
|
101
107
|
});
|
|
102
108
|
const rootStyle = useMemo(() => [resolvedSwitchStyles.root, switchStaticStyles.root({ disabled })], [resolvedSwitchStyles.root, disabled]);
|
|
109
|
+
const scaleDimensions = useCallback((style) => {
|
|
110
|
+
if (!style || fontScaleFactor === 1) return;
|
|
111
|
+
const { width, height } = style;
|
|
112
|
+
return typeof width === "number" && typeof height === "number" ? {
|
|
113
|
+
width: Math.round(width * fontScaleFactor),
|
|
114
|
+
height: Math.round(height * fontScaleFactor)
|
|
115
|
+
} : void 0;
|
|
116
|
+
}, [fontScaleFactor]);
|
|
103
117
|
const trackStyle = useMemo(() => [
|
|
104
118
|
resolvedSwitchStyles.switch,
|
|
105
119
|
switchStaticStyles.track,
|
|
106
120
|
variantLayerStyles.switch,
|
|
121
|
+
scaleDimensions(resolvedSwitchStyles.switch),
|
|
107
122
|
Platform.OS !== "web" && animatedTrackStyle
|
|
108
123
|
], [
|
|
109
124
|
resolvedSwitchStyles.switch,
|
|
110
125
|
variantLayerStyles.switch,
|
|
126
|
+
scaleDimensions,
|
|
111
127
|
animatedTrackStyle
|
|
112
128
|
]);
|
|
113
129
|
const handleStyle = useMemo(() => [
|
|
114
130
|
resolvedSwitchStyles.handle,
|
|
115
131
|
switchStaticStyles.handle,
|
|
116
132
|
variantLayerStyles.handle,
|
|
133
|
+
scaleDimensions(resolvedSwitchStyles.handle),
|
|
117
134
|
animatedHandleStyle
|
|
118
135
|
], [
|
|
119
136
|
resolvedSwitchStyles.handle,
|
|
120
137
|
variantLayerStyles.handle,
|
|
138
|
+
scaleDimensions,
|
|
121
139
|
animatedHandleStyle
|
|
122
140
|
]);
|
|
123
141
|
const accessibilityLabel = typeof label === "string" ? label : void 0;
|
|
@@ -128,6 +146,7 @@ const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onCha
|
|
|
128
146
|
label,
|
|
129
147
|
required,
|
|
130
148
|
showRequiredAsterisk: required,
|
|
149
|
+
maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
|
|
131
150
|
style: [resolvedSwitchStyles.text, variantLayerStyles.text]
|
|
132
151
|
});
|
|
133
152
|
const a11yValue = useMemo(() => ({ text: isOn ? "On" : "Off" }), [isOn]);
|
|
@@ -158,6 +177,8 @@ const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onCha
|
|
|
158
177
|
children: /* @__PURE__ */ jsx(IconSlot, {
|
|
159
178
|
icon: onIcon,
|
|
160
179
|
variant: "fill",
|
|
180
|
+
dangerouslySetSize: scaledHandleIconSize,
|
|
181
|
+
allowFontScaling: false,
|
|
161
182
|
style: [resolvedSwitchStyles.handleIcon, variantLayerStyles.handleIcon]
|
|
162
183
|
})
|
|
163
184
|
}), offIcon && !isOn && /* @__PURE__ */ jsx(Animated.View, {
|
|
@@ -165,6 +186,8 @@ const Switch = memo(function Switch({ isOn: isOnProp, defaultIsOn = false, onCha
|
|
|
165
186
|
children: /* @__PURE__ */ jsx(IconSlot, {
|
|
166
187
|
icon: offIcon,
|
|
167
188
|
variant: "fill",
|
|
189
|
+
dangerouslySetSize: scaledHandleIconSize,
|
|
190
|
+
allowFontScaling: false,
|
|
168
191
|
style: [resolvedSwitchStyles.handleIcon, variantLayerStyles.handleIcon]
|
|
169
192
|
})
|
|
170
193
|
})]
|
|
@@ -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 {\n AccessibilityProps,\n StyleProp,\n TextStyle,\n View,\n ViewProps,\n ViewStyle,\n} from 'react-native';\nimport { AccessibilityInfo, I18nManager, 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 // RTL mirrors the track layout but not transforms, so travel flips sign\n const travelDistance = HANDLE_TRAVEL[size] * (I18nManager.isRTL ? -1 : 1);\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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"}
|
|
1
|
+
{"version":3,"file":"Switch.js","names":["StyleSheet"],"sources":["../../src/components/Switch.tsx"],"sourcesContent":["import { ICON_SIZE_MAP } from '@yahoo/uds-icons/tokens';\nimport 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, I18nManager, 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 { useFontScale } from '../fontScaling/useFontScale';\nimport { useMaxFontSizeMultiplier } from '../fontScaling/useMaxFontSizeMultiplier';\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 /**\n * Caps how far the track, handle, and label grow with the OS text-size\n * setting; all scale by the same factor. Set null to remove the cap;\n * override app-wide via UDSFontScalingProvider.\n * @default 2\n */\n maxFontSizeMultiplier?: number | null;\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 maxFontSizeMultiplier,\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 // One capped factor governs track, handle, travel, and label.\n const resolvedMaxFontSizeMultiplier =\n useMaxFontSizeMultiplier('control', maxFontSizeMultiplier) ?? 0;\n const fontScaleFactor = useFontScale(resolvedMaxFontSizeMultiplier);\n // Size the handle glyph from the same factor as the handle it sits in, rather\n // than letting native font scaling size it on a separate path.\n const scaledHandleIconSize = Math.round(ICON_SIZE_MAP.sm * fontScaleFactor);\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 // RTL mirrors the track layout but not transforms, so travel flips sign.\n // Travel scales with the track/handle so the handle still lands flush.\n const travelDistance =\n Math.round(HANDLE_TRAVEL[size] * fontScaleFactor) * (I18nManager.isRTL ? -1 : 1);\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 | 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 // Track and handle grow by the capped factor so the handle (and its icon,\n // which native scales by the same capped multiplier) stays contained.\n const scaleDimensions = useCallback(\n (style: ViewStyle | undefined): ViewStyle | undefined => {\n if (!style || fontScaleFactor === 1) {\n return undefined;\n }\n const { width, height } = style;\n return typeof width === 'number' && typeof height === 'number'\n ? {\n width: Math.round(width * fontScaleFactor),\n height: Math.round(height * fontScaleFactor),\n }\n : undefined;\n },\n [fontScaleFactor],\n );\n\n const trackStyle: StyleProp<ViewStyle> = useMemo(\n () => [\n resolvedSwitchStyles.switch,\n switchStaticStyles.track,\n variantLayerStyles.switch,\n scaleDimensions(resolvedSwitchStyles.switch as ViewStyle),\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, scaleDimensions, animatedTrackStyle],\n );\n\n const handleStyle: StyleProp<ViewStyle> = useMemo(\n () => [\n resolvedSwitchStyles.handle,\n switchStaticStyles.handle,\n variantLayerStyles.handle,\n scaleDimensions(resolvedSwitchStyles.handle as ViewStyle),\n animatedHandleStyle,\n ],\n [resolvedSwitchStyles.handle, variantLayerStyles.handle, scaleDimensions, 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 maxFontSizeMultiplier={resolvedMaxFontSizeMultiplier}\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 dangerouslySetSize={scaledHandleIconSize}\n allowFontScaling={false}\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 dangerouslySetSize={scaledHandleIconSize}\n allowFontScaling={false}\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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"}
|