@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.
Files changed (47) hide show
  1. package/package.json +4 -4
  2. package/src/Dialog/Dialog.native.tsx +1 -1
  3. package/src/Form/Form.native.tsx +38 -0
  4. package/src/Form/Form.styles.tsx +14 -0
  5. package/src/Form/Form.web.tsx +50 -0
  6. package/src/Form/FormContext.ts +12 -0
  7. package/src/Form/fields/FormCheckbox.tsx +27 -0
  8. package/src/Form/fields/FormField.tsx +11 -0
  9. package/src/Form/fields/FormRadioGroup.tsx +24 -0
  10. package/src/Form/fields/FormSelect.tsx +27 -0
  11. package/src/Form/fields/FormSlider.tsx +26 -0
  12. package/src/Form/fields/FormSwitch.tsx +26 -0
  13. package/src/Form/fields/FormTextArea.tsx +27 -0
  14. package/src/Form/fields/FormTextInput.tsx +55 -0
  15. package/src/Form/index.native.ts +35 -0
  16. package/src/Form/index.ts +35 -0
  17. package/src/Form/index.web.ts +35 -0
  18. package/src/Form/types.ts +105 -0
  19. package/src/Form/useForm.ts +279 -0
  20. package/src/Form/useFormField.ts +21 -0
  21. package/src/Popover/Popover.native.tsx +1 -1
  22. package/src/RadioButton/RadioButton.styles.tsx +28 -0
  23. package/src/RadioButton/RadioGroup.native.tsx +28 -3
  24. package/src/RadioButton/RadioGroup.web.tsx +37 -1
  25. package/src/RadioButton/types.ts +16 -0
  26. package/src/Screen/Screen.native.tsx +12 -3
  27. package/src/Select/Select.native.tsx +10 -6
  28. package/src/Select/Select.styles.tsx +8 -8
  29. package/src/Select/Select.web.tsx +11 -7
  30. package/src/Select/types.ts +4 -2
  31. package/src/Slider/Slider.native.tsx +122 -0
  32. package/src/Slider/Slider.styles.tsx +48 -11
  33. package/src/Slider/Slider.web.tsx +54 -5
  34. package/src/Slider/types.ts +15 -0
  35. package/src/Switch/Switch.native.tsx +48 -20
  36. package/src/Switch/Switch.styles.tsx +28 -0
  37. package/src/Switch/Switch.web.tsx +55 -16
  38. package/src/Switch/types.ts +10 -0
  39. package/src/TextInput/TextInput.native.tsx +123 -40
  40. package/src/TextInput/TextInput.styles.tsx +47 -9
  41. package/src/TextInput/TextInput.web.tsx +163 -51
  42. package/src/TextInput/types.ts +16 -1
  43. package/src/hooks/useSmartPosition.native.ts +1 -1
  44. package/src/index.native.ts +19 -0
  45. package/src/index.ts +19 -0
  46. package/src/internal/BoundedModalContent.native.tsx +1 -1
  47. 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 = false,
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
- error,
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, error, focused: isOpen });
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)({ error });
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>
@@ -62,9 +62,11 @@ export interface SelectProps extends FormInputStyleProps, FormAccessibilityProps
62
62
  disabled?: boolean;
63
63
 
64
64
  /**
65
- * Whether the select shows an error state
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 wrapperProps = getWebProps([sliderStyles.sliderWrapper as any]);
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
- return (
256
- <div {...containerProps} ref={mergedRef} id={sliderId} data-testid={testID}>
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 {...wrapperProps}>
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';
@@ -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
- if (label) {
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
- <Pressable
160
- ref={ref as any}
161
- nativeID={id}
162
- onPress={handlePress}
163
- disabled={disabled}
164
- style={[switchStyles.container as any, style]}
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
- </Pressable>
201
+ </View>
174
202
  );
175
203
  }
176
204
 
177
- return switchElement;
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
  }));