@idealyst/components 1.2.106 → 1.2.108
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/Dialog/Dialog.native.tsx +1 -1
- 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/Popover/Popover.native.tsx +1 -1
- 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 +12 -3
- 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/hooks/useSmartPosition.native.ts +1 -1
- package/src/index.native.ts +19 -0
- package/src/index.ts +19 -0
- package/src/internal/BoundedModalContent.native.tsx +1 -1
- package/src/internal/SafeAreaDebugOverlay.native.tsx +1 -1
|
@@ -18,7 +18,7 @@ const Select = forwardRef<IdealystElement, SelectProps>(({
|
|
|
18
18
|
onChange,
|
|
19
19
|
placeholder = 'Select an option',
|
|
20
20
|
disabled = false,
|
|
21
|
-
error
|
|
21
|
+
error,
|
|
22
22
|
helperText,
|
|
23
23
|
label,
|
|
24
24
|
type = 'outlined',
|
|
@@ -36,6 +36,10 @@ const Select = forwardRef<IdealystElement, SelectProps>(({
|
|
|
36
36
|
accessibilityLabel,
|
|
37
37
|
id,
|
|
38
38
|
}, ref) => {
|
|
39
|
+
// Derive hasError boolean from error prop
|
|
40
|
+
const hasError = Boolean(error);
|
|
41
|
+
// Get error message if error is a string
|
|
42
|
+
const errorMessage = typeof error === 'string' ? error : undefined;
|
|
39
43
|
const [isOpen, setIsOpen] = useState(false);
|
|
40
44
|
const [searchTerm, setSearchTerm] = useState('');
|
|
41
45
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
@@ -66,7 +70,7 @@ const Select = forwardRef<IdealystElement, SelectProps>(({
|
|
|
66
70
|
type,
|
|
67
71
|
size,
|
|
68
72
|
disabled,
|
|
69
|
-
|
|
73
|
+
hasError,
|
|
70
74
|
focused: isOpen,
|
|
71
75
|
margin,
|
|
72
76
|
marginVertical,
|
|
@@ -182,7 +186,7 @@ const Select = forwardRef<IdealystElement, SelectProps>(({
|
|
|
182
186
|
// Get dynamic styles - call as functions for theme reactivity
|
|
183
187
|
const containerStyle = (selectStyles.container as any)({});
|
|
184
188
|
const labelStyle = (selectStyles.label as any)({});
|
|
185
|
-
const triggerStyle = (selectStyles.trigger as any)({ type, intent, disabled,
|
|
189
|
+
const triggerStyle = (selectStyles.trigger as any)({ type, intent, disabled, hasError, focused: isOpen });
|
|
186
190
|
const triggerContentStyle = (selectStyles.triggerContent as any)({});
|
|
187
191
|
const triggerTextStyle = (selectStyles.triggerText as any)({});
|
|
188
192
|
const placeholderStyle = (selectStyles.placeholder as any)({});
|
|
@@ -200,7 +204,7 @@ const Select = forwardRef<IdealystElement, SelectProps>(({
|
|
|
200
204
|
const optionIconStyle = (selectStyles.optionIcon as any)({});
|
|
201
205
|
const optionTextStyle = (selectStyles.optionText as any)({});
|
|
202
206
|
const optionTextDisabledStyle = (selectStyles.optionTextDisabled as any)({});
|
|
203
|
-
const helperTextStyle = (selectStyles.helperText as any)({
|
|
207
|
+
const helperTextStyle = (selectStyles.helperText as any)({ hasError });
|
|
204
208
|
|
|
205
209
|
const containerWebProps = getWebProps([containerStyle, style as any]);
|
|
206
210
|
const triggerWebProps = getWebProps([triggerStyle]);
|
|
@@ -336,9 +340,9 @@ const Select = forwardRef<IdealystElement, SelectProps>(({
|
|
|
336
340
|
</div>
|
|
337
341
|
</PositionedPortal>
|
|
338
342
|
|
|
339
|
-
{helperText && (
|
|
340
|
-
<div {...getWebProps([helperTextStyle])}>
|
|
341
|
-
{helperText}
|
|
343
|
+
{(errorMessage || helperText) && (
|
|
344
|
+
<div {...getWebProps([helperTextStyle])} role={errorMessage ? 'alert' : undefined}>
|
|
345
|
+
{errorMessage || helperText}
|
|
342
346
|
</div>
|
|
343
347
|
)}
|
|
344
348
|
</div>
|
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
|
}));
|