@idealyst/components 1.2.105 → 1.2.107
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/package.json +4 -4
- package/src/Form/Form.native.tsx +38 -0
- package/src/Form/Form.styles.tsx +14 -0
- package/src/Form/Form.web.tsx +50 -0
- package/src/Form/FormContext.ts +12 -0
- package/src/Form/fields/FormCheckbox.tsx +27 -0
- package/src/Form/fields/FormField.tsx +11 -0
- package/src/Form/fields/FormRadioGroup.tsx +24 -0
- package/src/Form/fields/FormSelect.tsx +27 -0
- package/src/Form/fields/FormSlider.tsx +26 -0
- package/src/Form/fields/FormSwitch.tsx +26 -0
- package/src/Form/fields/FormTextArea.tsx +27 -0
- package/src/Form/fields/FormTextInput.tsx +55 -0
- package/src/Form/index.native.ts +35 -0
- package/src/Form/index.ts +35 -0
- package/src/Form/index.web.ts +35 -0
- package/src/Form/types.ts +105 -0
- package/src/Form/useForm.ts +279 -0
- package/src/Form/useFormField.ts +21 -0
- package/src/RadioButton/RadioButton.styles.tsx +28 -0
- package/src/RadioButton/RadioGroup.native.tsx +28 -3
- package/src/RadioButton/RadioGroup.web.tsx +37 -1
- package/src/RadioButton/types.ts +16 -0
- package/src/Screen/Screen.native.tsx +11 -2
- package/src/Select/Select.native.tsx +10 -6
- package/src/Select/Select.styles.tsx +8 -8
- package/src/Select/Select.web.tsx +11 -7
- package/src/Select/types.ts +4 -2
- package/src/Slider/Slider.native.tsx +122 -0
- package/src/Slider/Slider.styles.tsx +48 -11
- package/src/Slider/Slider.web.tsx +54 -5
- package/src/Slider/types.ts +15 -0
- package/src/Switch/Switch.native.tsx +48 -20
- package/src/Switch/Switch.styles.tsx +28 -0
- package/src/Switch/Switch.web.tsx +55 -16
- package/src/Switch/types.ts +10 -0
- package/src/TextInput/TextInput.native.tsx +123 -40
- package/src/TextInput/TextInput.styles.tsx +47 -9
- package/src/TextInput/TextInput.web.tsx +163 -51
- package/src/TextInput/types.ts +16 -1
- package/src/index.native.ts +19 -0
- package/src/index.ts +19 -0
package/src/Select/types.ts
CHANGED
|
@@ -62,9 +62,11 @@ export interface SelectProps extends FormInputStyleProps, FormAccessibilityProps
|
|
|
62
62
|
disabled?: boolean;
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
65
|
+
* Error state or error message. When a string is provided, it displays as error text.
|
|
66
|
+
* When boolean true, shows error styling without text.
|
|
67
|
+
* @deprecated Using boolean is deprecated. Prefer passing an error message string.
|
|
66
68
|
*/
|
|
67
|
-
error?: boolean;
|
|
69
|
+
error?: string | boolean;
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
72
|
* Helper text to display below the select
|
|
@@ -17,6 +17,9 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
17
17
|
max = 100,
|
|
18
18
|
step = 1,
|
|
19
19
|
disabled = false,
|
|
20
|
+
error,
|
|
21
|
+
helperText,
|
|
22
|
+
label,
|
|
20
23
|
showValue = false,
|
|
21
24
|
showMinMax = false,
|
|
22
25
|
marks = [],
|
|
@@ -43,6 +46,11 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
43
46
|
accessibilityValueMax,
|
|
44
47
|
accessibilityValueText,
|
|
45
48
|
}, ref) => {
|
|
49
|
+
// Derive hasError from error prop
|
|
50
|
+
const hasError = Boolean(error);
|
|
51
|
+
// Determine if we need a wrapper (when label, error, or helperText is present)
|
|
52
|
+
const needsWrapper = Boolean(label) || Boolean(error) || Boolean(helperText);
|
|
53
|
+
const showFooter = Boolean(error) || Boolean(helperText);
|
|
46
54
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
47
55
|
const [trackWidthState, setTrackWidthState] = useState(0);
|
|
48
56
|
const trackWidth = useSharedValue(0);
|
|
@@ -55,11 +63,18 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
55
63
|
sliderStyles.useVariants({
|
|
56
64
|
size,
|
|
57
65
|
disabled,
|
|
66
|
+
hasError,
|
|
58
67
|
margin,
|
|
59
68
|
marginVertical,
|
|
60
69
|
marginHorizontal,
|
|
61
70
|
});
|
|
62
71
|
|
|
72
|
+
// Wrapper, label, and footer styles
|
|
73
|
+
const wrapperStyle = sliderStyles.wrapper as any;
|
|
74
|
+
const labelStyle = sliderStyles.label as any;
|
|
75
|
+
const footerStyle = sliderStyles.footer as any;
|
|
76
|
+
const helperTextStyle = sliderStyles.helperText as any;
|
|
77
|
+
|
|
63
78
|
const clampValue = (val: number) => {
|
|
64
79
|
'worklet';
|
|
65
80
|
const clampedValue = Math.min(Math.max(val, min), max);
|
|
@@ -211,6 +226,113 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
211
226
|
const containerStyle = (sliderStyles.container as any);
|
|
212
227
|
const trackStyle = (sliderStyles.track as any);
|
|
213
228
|
|
|
229
|
+
// The slider container element
|
|
230
|
+
const sliderContainer = (
|
|
231
|
+
<View style={[containerStyle, !needsWrapper && style]} testID={!needsWrapper ? testID : undefined} {...nativeA11yProps}>
|
|
232
|
+
{showValue && (
|
|
233
|
+
<View style={sliderStyles.valueLabel as any}>
|
|
234
|
+
<Text>{value}</Text>
|
|
235
|
+
</View>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
<View style={sliderStyles.sliderWrapper}>
|
|
239
|
+
<GestureDetector gesture={composedGesture}>
|
|
240
|
+
<View
|
|
241
|
+
style={trackStyle}
|
|
242
|
+
onLayout={(e: LayoutChangeEvent) => {
|
|
243
|
+
const width = e.nativeEvent.layout.width;
|
|
244
|
+
trackWidth.value = width;
|
|
245
|
+
setTrackWidthState(width);
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
{/* Filled track */}
|
|
249
|
+
<Animated.View
|
|
250
|
+
style={[(sliderStyles.filledTrack as any), filledTrackAnimatedStyle]}
|
|
251
|
+
/>
|
|
252
|
+
|
|
253
|
+
{/* Marks */}
|
|
254
|
+
{marks.length > 0 && trackWidthState > 0 && (
|
|
255
|
+
<View style={sliderStyles.marks as any}>
|
|
256
|
+
{marks.map((mark) => {
|
|
257
|
+
const markPercentage = ((mark.value - min) / (max - min)) * 100;
|
|
258
|
+
const markPosition = (markPercentage / 100) * trackWidthState;
|
|
259
|
+
return (
|
|
260
|
+
<View key={mark.value}>
|
|
261
|
+
<View
|
|
262
|
+
style={[
|
|
263
|
+
sliderStyles.mark as any,
|
|
264
|
+
{ left: markPosition },
|
|
265
|
+
]}
|
|
266
|
+
/>
|
|
267
|
+
{mark.label && (
|
|
268
|
+
<View
|
|
269
|
+
style={[
|
|
270
|
+
sliderStyles.markLabel as any,
|
|
271
|
+
{ left: markPosition },
|
|
272
|
+
]}
|
|
273
|
+
>
|
|
274
|
+
<Text typography="caption">{mark.label}</Text>
|
|
275
|
+
</View>
|
|
276
|
+
)}
|
|
277
|
+
</View>
|
|
278
|
+
);
|
|
279
|
+
})}
|
|
280
|
+
</View>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{/* Thumb */}
|
|
284
|
+
<Animated.View
|
|
285
|
+
style={[
|
|
286
|
+
(sliderStyles.thumb as any),
|
|
287
|
+
{
|
|
288
|
+
// Manual positioning/sizing for native layout
|
|
289
|
+
position: 'absolute',
|
|
290
|
+
top: '50%',
|
|
291
|
+
marginTop: -thumbSize / 2,
|
|
292
|
+
width: thumbSize,
|
|
293
|
+
height: thumbSize,
|
|
294
|
+
borderRadius: thumbSize / 2,
|
|
295
|
+
},
|
|
296
|
+
thumbAnimatedStyle,
|
|
297
|
+
]}
|
|
298
|
+
>
|
|
299
|
+
{renderIcon()}
|
|
300
|
+
</Animated.View>
|
|
301
|
+
</View>
|
|
302
|
+
</GestureDetector>
|
|
303
|
+
</View>
|
|
304
|
+
|
|
305
|
+
{showMinMax && (
|
|
306
|
+
<View style={sliderStyles.minMaxLabels}>
|
|
307
|
+
<Text style={sliderStyles.minMaxLabel} typography="caption">{min}</Text>
|
|
308
|
+
<Text style={sliderStyles.minMaxLabel} typography="caption">{max}</Text>
|
|
309
|
+
</View>
|
|
310
|
+
)}
|
|
311
|
+
</View>
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// If wrapper needed for label/error/helperText
|
|
315
|
+
if (needsWrapper) {
|
|
316
|
+
return (
|
|
317
|
+
<View ref={ref as any} nativeID={id} style={[wrapperStyle, style]} testID={testID}>
|
|
318
|
+
{label && (
|
|
319
|
+
<Text style={labelStyle}>{label}</Text>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{sliderContainer}
|
|
323
|
+
|
|
324
|
+
{showFooter && (
|
|
325
|
+
<View style={footerStyle}>
|
|
326
|
+
<View style={{ flex: 1 }}>
|
|
327
|
+
{error && <Text style={helperTextStyle}>{error}</Text>}
|
|
328
|
+
{!error && helperText && <Text style={helperTextStyle}>{helperText}</Text>}
|
|
329
|
+
</View>
|
|
330
|
+
</View>
|
|
331
|
+
)}
|
|
332
|
+
</View>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
214
336
|
return (
|
|
215
337
|
<View ref={ref as any} nativeID={id} style={[containerStyle, style]} testID={testID} {...nativeA11yProps}>
|
|
216
338
|
{showValue && (
|
|
@@ -16,6 +16,7 @@ export type SliderVariants = {
|
|
|
16
16
|
size: Size;
|
|
17
17
|
intent: Intent;
|
|
18
18
|
disabled: boolean;
|
|
19
|
+
hasError?: boolean;
|
|
19
20
|
margin?: ViewStyleSize;
|
|
20
21
|
marginVertical?: ViewStyleSize;
|
|
21
22
|
marginHorizontal?: ViewStyleSize;
|
|
@@ -55,17 +56,6 @@ export const sliderStyles = defineStyle('Slider', (theme: Theme) => ({
|
|
|
55
56
|
container: {
|
|
56
57
|
gap: 4,
|
|
57
58
|
paddingVertical: 8,
|
|
58
|
-
variants: {
|
|
59
|
-
margin: {
|
|
60
|
-
margin: theme.sizes.$view.padding,
|
|
61
|
-
},
|
|
62
|
-
marginVertical: {
|
|
63
|
-
marginVertical: theme.sizes.$view.padding,
|
|
64
|
-
},
|
|
65
|
-
marginHorizontal: {
|
|
66
|
-
marginHorizontal: theme.sizes.$view.padding,
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
59
|
},
|
|
70
60
|
|
|
71
61
|
sliderWrapper: {
|
|
@@ -266,4 +256,51 @@ export const sliderStyles = defineStyle('Slider', (theme: Theme) => ({
|
|
|
266
256
|
whiteSpace: 'nowrap',
|
|
267
257
|
},
|
|
268
258
|
},
|
|
259
|
+
|
|
260
|
+
wrapper: {
|
|
261
|
+
display: 'flex' as const,
|
|
262
|
+
flexDirection: 'column' as const,
|
|
263
|
+
gap: 4,
|
|
264
|
+
variants: {
|
|
265
|
+
margin: {
|
|
266
|
+
margin: theme.sizes.$view.padding,
|
|
267
|
+
},
|
|
268
|
+
marginVertical: {
|
|
269
|
+
marginVertical: theme.sizes.$view.padding,
|
|
270
|
+
},
|
|
271
|
+
marginHorizontal: {
|
|
272
|
+
marginHorizontal: theme.sizes.$view.padding,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
label: {
|
|
278
|
+
fontSize: 14,
|
|
279
|
+
fontWeight: '500' as const,
|
|
280
|
+
color: theme.colors.text.primary,
|
|
281
|
+
variants: {
|
|
282
|
+
disabled: {
|
|
283
|
+
true: { opacity: 0.5 },
|
|
284
|
+
false: { opacity: 1 },
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
helperText: {
|
|
290
|
+
fontSize: 12,
|
|
291
|
+
variants: {
|
|
292
|
+
hasError: {
|
|
293
|
+
true: { color: theme.intents.danger.primary },
|
|
294
|
+
false: { color: theme.colors.text.secondary },
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
footer: {
|
|
300
|
+
display: 'flex' as const,
|
|
301
|
+
flexDirection: 'row' as const,
|
|
302
|
+
justifyContent: 'space-between' as const,
|
|
303
|
+
alignItems: 'center' as const,
|
|
304
|
+
gap: 4,
|
|
305
|
+
},
|
|
269
306
|
}));
|
|
@@ -19,6 +19,9 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
19
19
|
max = 100,
|
|
20
20
|
step = 1,
|
|
21
21
|
disabled = false,
|
|
22
|
+
error,
|
|
23
|
+
helperText,
|
|
24
|
+
label,
|
|
22
25
|
showValue = false,
|
|
23
26
|
showMinMax = false,
|
|
24
27
|
marks = [],
|
|
@@ -45,6 +48,10 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
45
48
|
accessibilityValueMax,
|
|
46
49
|
accessibilityValueText,
|
|
47
50
|
}, ref) => {
|
|
51
|
+
// Derive hasError from error prop
|
|
52
|
+
const hasError = Boolean(error);
|
|
53
|
+
// Determine if we need a wrapper (when label, error, or helperText is present)
|
|
54
|
+
const needsWrapper = Boolean(label) || Boolean(error) || Boolean(helperText);
|
|
48
55
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
49
56
|
const [isDragging, setIsDragging] = useState(false);
|
|
50
57
|
const trackRef = useRef<HTMLDivElement>(null);
|
|
@@ -58,13 +65,14 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
58
65
|
size,
|
|
59
66
|
intent,
|
|
60
67
|
disabled,
|
|
68
|
+
hasError,
|
|
61
69
|
margin,
|
|
62
70
|
marginVertical,
|
|
63
71
|
marginHorizontal,
|
|
64
72
|
});
|
|
65
73
|
|
|
66
|
-
const containerProps = getWebProps([sliderStyles.container as any, style as any]);
|
|
67
|
-
const
|
|
74
|
+
const containerProps = getWebProps([sliderStyles.container as any, !needsWrapper && style as any].filter(Boolean));
|
|
75
|
+
const sliderWrapperProps = getWebProps([sliderStyles.sliderWrapper as any]);
|
|
68
76
|
const trackProps = getWebProps([sliderStyles.track as any]);
|
|
69
77
|
const thumbIconProps = getWebProps([sliderStyles.thumbIcon as any]);
|
|
70
78
|
const valueLabelProps = getWebProps([sliderStyles.valueLabel as any]);
|
|
@@ -72,6 +80,14 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
72
80
|
const minMaxLabelProps = getWebProps([sliderStyles.minMaxLabel as any]);
|
|
73
81
|
const marksProps = getWebProps([sliderStyles.marks as any]);
|
|
74
82
|
|
|
83
|
+
// Wrapper, label, and footer styles
|
|
84
|
+
const outerWrapperProps = getWebProps([sliderStyles.wrapper as any, style as any]);
|
|
85
|
+
const labelProps = getWebProps([sliderStyles.label as any]);
|
|
86
|
+
const footerProps = getWebProps([sliderStyles.footer as any]);
|
|
87
|
+
const helperTextProps = getWebProps([sliderStyles.helperText as any]);
|
|
88
|
+
|
|
89
|
+
const showFooter = Boolean(error) || Boolean(helperText);
|
|
90
|
+
|
|
75
91
|
const clampValue = useCallback((val: number) => {
|
|
76
92
|
const clampedValue = Math.min(Math.max(val, min), max);
|
|
77
93
|
const steppedValue = Math.round(clampedValue / step) * step;
|
|
@@ -252,15 +268,16 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
252
268
|
|
|
253
269
|
const mergedRef = useMergeRefs(ref, containerProps.ref);
|
|
254
270
|
|
|
255
|
-
|
|
256
|
-
|
|
271
|
+
// The slider container element
|
|
272
|
+
const sliderContainer = (
|
|
273
|
+
<div {...containerProps} ref={!needsWrapper ? mergedRef : undefined} id={!needsWrapper ? sliderId : undefined} data-testid={!needsWrapper ? testID : undefined}>
|
|
257
274
|
{showValue && (
|
|
258
275
|
<div {...valueLabelProps}>
|
|
259
276
|
{value}
|
|
260
277
|
</div>
|
|
261
278
|
)}
|
|
262
279
|
|
|
263
|
-
<div {...
|
|
280
|
+
<div {...sliderWrapperProps}>
|
|
264
281
|
<div
|
|
265
282
|
{...trackProps}
|
|
266
283
|
{...ariaProps}
|
|
@@ -311,6 +328,38 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
311
328
|
)}
|
|
312
329
|
</div>
|
|
313
330
|
);
|
|
331
|
+
|
|
332
|
+
// If wrapper needed for label/error/helperText
|
|
333
|
+
if (needsWrapper) {
|
|
334
|
+
return (
|
|
335
|
+
<div {...outerWrapperProps} id={sliderId} data-testid={testID}>
|
|
336
|
+
{label && (
|
|
337
|
+
<label {...labelProps}>{label}</label>
|
|
338
|
+
)}
|
|
339
|
+
|
|
340
|
+
{sliderContainer}
|
|
341
|
+
|
|
342
|
+
{showFooter && (
|
|
343
|
+
<div {...footerProps}>
|
|
344
|
+
<div style={{ flex: 1 }}>
|
|
345
|
+
{error && (
|
|
346
|
+
<span {...helperTextProps} role="alert">
|
|
347
|
+
{error}
|
|
348
|
+
</span>
|
|
349
|
+
)}
|
|
350
|
+
{!error && helperText && (
|
|
351
|
+
<span {...helperTextProps}>
|
|
352
|
+
{helperText}
|
|
353
|
+
</span>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return sliderContainer;
|
|
314
363
|
});
|
|
315
364
|
|
|
316
365
|
Slider.displayName = 'Slider';
|
package/src/Slider/types.ts
CHANGED
|
@@ -22,6 +22,21 @@ export interface SliderProps extends FormInputStyleProps, RangeAccessibilityProp
|
|
|
22
22
|
max?: number;
|
|
23
23
|
step?: number;
|
|
24
24
|
disabled?: boolean;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Error message to display below the slider. When set, shows error styling.
|
|
28
|
+
*/
|
|
29
|
+
error?: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Helper text to display below the slider. Hidden when error is set.
|
|
33
|
+
*/
|
|
34
|
+
helperText?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Label text to display above the slider
|
|
38
|
+
*/
|
|
39
|
+
label?: string;
|
|
25
40
|
showValue?: boolean;
|
|
26
41
|
showMinMax?: boolean;
|
|
27
42
|
marks?: SliderMark[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { forwardRef, useMemo } from 'react';
|
|
2
|
-
import { Pressable } from 'react-native';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
3
|
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
|
4
4
|
import { switchStyles } from './Switch.styles';
|
|
5
5
|
import Text from '../Text';
|
|
@@ -11,6 +11,8 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
11
11
|
checked = false,
|
|
12
12
|
onChange,
|
|
13
13
|
disabled = false,
|
|
14
|
+
error,
|
|
15
|
+
helperText,
|
|
14
16
|
label,
|
|
15
17
|
labelPosition = 'right',
|
|
16
18
|
intent = 'primary',
|
|
@@ -32,10 +34,17 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
32
34
|
accessibilityDescribedBy,
|
|
33
35
|
accessibilityChecked,
|
|
34
36
|
}, ref) => {
|
|
37
|
+
// Derive hasError from error prop
|
|
38
|
+
const hasError = Boolean(error);
|
|
39
|
+
// Determine if we need a wrapper (when error or helperText is present)
|
|
40
|
+
const needsWrapper = Boolean(error) || Boolean(helperText);
|
|
41
|
+
const showFooter = Boolean(error) || Boolean(helperText);
|
|
42
|
+
|
|
35
43
|
switchStyles.useVariants({
|
|
36
44
|
size,
|
|
37
45
|
disabled,
|
|
38
46
|
checked,
|
|
47
|
+
hasError,
|
|
39
48
|
intent,
|
|
40
49
|
position: labelPosition,
|
|
41
50
|
margin,
|
|
@@ -43,6 +52,10 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
43
52
|
marginHorizontal,
|
|
44
53
|
});
|
|
45
54
|
|
|
55
|
+
// Wrapper and helperText styles
|
|
56
|
+
const wrapperStyle = (switchStyles.wrapper as any)({});
|
|
57
|
+
const helperTextStyle = (switchStyles.helperText as any)({ hasError });
|
|
58
|
+
|
|
46
59
|
const progress = useSharedValue(checked ? 1 : 0);
|
|
47
60
|
|
|
48
61
|
React.useEffect(() => {
|
|
@@ -123,12 +136,12 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
123
136
|
|
|
124
137
|
const switchElement = (
|
|
125
138
|
<Pressable
|
|
126
|
-
ref={!label ? ref : undefined}
|
|
127
|
-
nativeID={!label ? id : undefined}
|
|
139
|
+
ref={!label && !needsWrapper ? ref : undefined}
|
|
140
|
+
nativeID={!label && !needsWrapper ? id : undefined}
|
|
128
141
|
onPress={handlePress}
|
|
129
142
|
disabled={disabled}
|
|
130
143
|
style={switchStyles.switchContainer as any}
|
|
131
|
-
testID={testID}
|
|
144
|
+
testID={!label && !needsWrapper ? testID : undefined}
|
|
132
145
|
{...nativeA11yProps}
|
|
133
146
|
>
|
|
134
147
|
<Animated.View style={switchStyles.switchTrack as any}>
|
|
@@ -154,27 +167,42 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
154
167
|
</Pressable>
|
|
155
168
|
);
|
|
156
169
|
|
|
157
|
-
|
|
170
|
+
// The switch + label row
|
|
171
|
+
const switchWithLabel = label ? (
|
|
172
|
+
<Pressable
|
|
173
|
+
ref={!needsWrapper ? ref as any : undefined}
|
|
174
|
+
nativeID={!needsWrapper ? id : undefined}
|
|
175
|
+
onPress={handlePress}
|
|
176
|
+
disabled={disabled}
|
|
177
|
+
style={[switchStyles.container as any, !needsWrapper && style]}
|
|
178
|
+
testID={!needsWrapper ? testID : undefined}
|
|
179
|
+
>
|
|
180
|
+
{labelPosition === 'left' && (
|
|
181
|
+
<Text style={switchStyles.label as any}>{label}</Text>
|
|
182
|
+
)}
|
|
183
|
+
{switchElement}
|
|
184
|
+
{labelPosition === 'right' && (
|
|
185
|
+
<Text style={switchStyles.label as any}>{label}</Text>
|
|
186
|
+
)}
|
|
187
|
+
</Pressable>
|
|
188
|
+
) : switchElement;
|
|
189
|
+
|
|
190
|
+
// If wrapper needed for error/helperText
|
|
191
|
+
if (needsWrapper) {
|
|
158
192
|
return (
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
{labelPosition === 'left' && (
|
|
167
|
-
<Text style={switchStyles.label as any}>{label}</Text>
|
|
168
|
-
)}
|
|
169
|
-
{switchElement}
|
|
170
|
-
{labelPosition === 'right' && (
|
|
171
|
-
<Text style={switchStyles.label as any}>{label}</Text>
|
|
193
|
+
<View ref={ref as any} nativeID={id} style={[wrapperStyle, style]} testID={testID}>
|
|
194
|
+
{switchWithLabel}
|
|
195
|
+
{showFooter && (
|
|
196
|
+
<View style={{ flex: 1 }}>
|
|
197
|
+
{error && <Text style={helperTextStyle}>{error}</Text>}
|
|
198
|
+
{!error && helperText && <Text style={helperTextStyle}>{helperText}</Text>}
|
|
199
|
+
</View>
|
|
172
200
|
)}
|
|
173
|
-
</
|
|
201
|
+
</View>
|
|
174
202
|
);
|
|
175
203
|
}
|
|
176
204
|
|
|
177
|
-
return
|
|
205
|
+
return switchWithLabel;
|
|
178
206
|
});
|
|
179
207
|
|
|
180
208
|
Switch.displayName = 'Switch';
|
|
@@ -19,6 +19,7 @@ export type SwitchDynamicProps = {
|
|
|
19
19
|
intent?: Intent;
|
|
20
20
|
checked?: boolean;
|
|
21
21
|
disabled?: boolean;
|
|
22
|
+
hasError?: boolean;
|
|
22
23
|
labelPosition?: LabelPosition;
|
|
23
24
|
margin?: ViewStyleSize;
|
|
24
25
|
marginVertical?: ViewStyleSize;
|
|
@@ -166,4 +167,31 @@ export const switchStyles = defineStyle('Switch', (theme: Theme) => ({
|
|
|
166
167
|
},
|
|
167
168
|
},
|
|
168
169
|
}),
|
|
170
|
+
|
|
171
|
+
wrapper: (_props: SwitchDynamicProps) => ({
|
|
172
|
+
display: 'flex' as const,
|
|
173
|
+
flexDirection: 'column' as const,
|
|
174
|
+
gap: 4,
|
|
175
|
+
variants: {
|
|
176
|
+
margin: {
|
|
177
|
+
margin: theme.sizes.$view.padding,
|
|
178
|
+
},
|
|
179
|
+
marginVertical: {
|
|
180
|
+
marginVertical: theme.sizes.$view.padding,
|
|
181
|
+
},
|
|
182
|
+
marginHorizontal: {
|
|
183
|
+
marginHorizontal: theme.sizes.$view.padding,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
|
|
188
|
+
helperText: (_props: SwitchDynamicProps) => ({
|
|
189
|
+
fontSize: 12,
|
|
190
|
+
variants: {
|
|
191
|
+
hasError: {
|
|
192
|
+
true: { color: theme.intents.danger.primary },
|
|
193
|
+
false: { color: theme.colors.text.secondary },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
169
197
|
}));
|
|
@@ -17,6 +17,8 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
17
17
|
checked = false,
|
|
18
18
|
onChange,
|
|
19
19
|
disabled = false,
|
|
20
|
+
error,
|
|
21
|
+
helperText,
|
|
20
22
|
label,
|
|
21
23
|
labelPosition = 'right',
|
|
22
24
|
intent = 'primary',
|
|
@@ -40,6 +42,10 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
40
42
|
accessibilityDescribedBy,
|
|
41
43
|
accessibilityChecked,
|
|
42
44
|
}, ref) => {
|
|
45
|
+
// Derive hasError from error prop
|
|
46
|
+
const hasError = Boolean(error);
|
|
47
|
+
// Determine if we need a wrapper (when error or helperText is present)
|
|
48
|
+
const needsWrapper = Boolean(error) || Boolean(helperText);
|
|
43
49
|
const handleClick = (e: React.MouseEvent) => {
|
|
44
50
|
e.preventDefault();
|
|
45
51
|
e.stopPropagation();
|
|
@@ -86,6 +92,7 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
86
92
|
size,
|
|
87
93
|
checked,
|
|
88
94
|
disabled: disabled as boolean,
|
|
95
|
+
hasError,
|
|
89
96
|
labelPosition,
|
|
90
97
|
margin,
|
|
91
98
|
intent,
|
|
@@ -98,6 +105,14 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
98
105
|
const thumbIconProps = getWebProps([switchStyles.thumbIcon as any]);
|
|
99
106
|
const labelProps = getWebProps([switchStyles.label as any]);
|
|
100
107
|
|
|
108
|
+
// Wrapper and helperText styles
|
|
109
|
+
const wrapperStyleComputed = (switchStyles.wrapper as any)({});
|
|
110
|
+
const helperTextStyleComputed = (switchStyles.helperText as any)({ hasError });
|
|
111
|
+
const wrapperProps = getWebProps([wrapperStyleComputed, flattenStyle(style)]);
|
|
112
|
+
const helperTextProps = getWebProps([helperTextStyleComputed]);
|
|
113
|
+
|
|
114
|
+
const showFooter = Boolean(error) || Boolean(helperText);
|
|
115
|
+
|
|
101
116
|
// Helper to render icon
|
|
102
117
|
const renderIcon = () => {
|
|
103
118
|
const iconToRender = checked ? onIcon : offIcon;
|
|
@@ -122,7 +137,7 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
122
137
|
const computedButtonProps = getWebProps([switchStyles.switchContainer as any]);
|
|
123
138
|
|
|
124
139
|
// Computed container props (for when label exists)
|
|
125
|
-
const computedContainerProps = getWebProps([switchStyles.container as any]);
|
|
140
|
+
const computedContainerProps = getWebProps([switchStyles.container as any, !needsWrapper && flattenStyle(style)].filter(Boolean));
|
|
126
141
|
|
|
127
142
|
const mergedButtonRef = useMergeRefs(ref as React.Ref<HTMLButtonElement>, computedButtonProps.ref);
|
|
128
143
|
const mergedContainerRef = useMergeRefs(ref as React.Ref<HTMLDivElement>, computedContainerProps.ref);
|
|
@@ -131,12 +146,12 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
131
146
|
<button
|
|
132
147
|
{...computedButtonProps}
|
|
133
148
|
{...ariaProps}
|
|
134
|
-
style={flattenStyle(style)}
|
|
135
|
-
ref={mergedButtonRef}
|
|
149
|
+
style={!label && !needsWrapper ? flattenStyle(style) : undefined}
|
|
150
|
+
ref={!label && !needsWrapper ? mergedButtonRef : undefined}
|
|
136
151
|
onClick={handleClick}
|
|
137
152
|
disabled={disabled}
|
|
138
153
|
id={switchId}
|
|
139
|
-
data-testid={testID}
|
|
154
|
+
data-testid={!label && !needsWrapper ? testID : undefined}
|
|
140
155
|
>
|
|
141
156
|
<div {...trackProps}>
|
|
142
157
|
<div {...thumbProps}>
|
|
@@ -146,24 +161,48 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
146
161
|
</button>
|
|
147
162
|
);
|
|
148
163
|
|
|
149
|
-
|
|
164
|
+
// The switch + label row
|
|
165
|
+
const switchWithLabel = label ? (
|
|
166
|
+
<div
|
|
167
|
+
{...computedContainerProps}
|
|
168
|
+
ref={!needsWrapper ? mergedContainerRef : undefined}
|
|
169
|
+
id={!needsWrapper ? id : undefined}
|
|
170
|
+
data-testid={!needsWrapper ? testID : undefined}
|
|
171
|
+
>
|
|
172
|
+
{labelPosition === 'left' && (
|
|
173
|
+
<span {...labelProps}>{label}</span>
|
|
174
|
+
)}
|
|
175
|
+
{switchElement}
|
|
176
|
+
{labelPosition === 'right' && (
|
|
177
|
+
<span {...labelProps}>{label}</span>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
) : switchElement;
|
|
181
|
+
|
|
182
|
+
// If wrapper needed for error/helperText
|
|
183
|
+
if (needsWrapper) {
|
|
150
184
|
return (
|
|
151
|
-
<div
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
185
|
+
<div {...wrapperProps} id={id} data-testid={testID}>
|
|
186
|
+
{switchWithLabel}
|
|
187
|
+
{showFooter && (
|
|
188
|
+
<div style={{ flex: 1 }}>
|
|
189
|
+
{error && (
|
|
190
|
+
<span {...helperTextProps} role="alert">
|
|
191
|
+
{error}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
{!error && helperText && (
|
|
195
|
+
<span {...helperTextProps}>
|
|
196
|
+
{helperText}
|
|
197
|
+
</span>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
161
200
|
)}
|
|
162
201
|
</div>
|
|
163
202
|
);
|
|
164
203
|
}
|
|
165
204
|
|
|
166
|
-
return
|
|
205
|
+
return switchWithLabel;
|
|
167
206
|
});
|
|
168
207
|
|
|
169
208
|
Switch.displayName = 'Switch';
|
package/src/Switch/types.ts
CHANGED
|
@@ -28,6 +28,16 @@ export interface SwitchProps extends FormInputStyleProps, SelectionAccessibility
|
|
|
28
28
|
*/
|
|
29
29
|
disabled?: boolean;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Error message to display below the switch. When set, shows error styling.
|
|
33
|
+
*/
|
|
34
|
+
error?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Helper text to display below the switch. Hidden when error is set.
|
|
38
|
+
*/
|
|
39
|
+
helperText?: string;
|
|
40
|
+
|
|
31
41
|
/**
|
|
32
42
|
* Label text to display next to the switch
|
|
33
43
|
*/
|