@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
@@ -6,7 +6,7 @@ import { isIconName } from '../Icon/icon-resolver';
6
6
  import useMergeRefs from '../hooks/useMergeRefs';
7
7
  import { textInputStyles } from './TextInput.styles';
8
8
  import { TextInputProps } from './types';
9
- import { getWebFormAriaProps } from '../utils/accessibility';
9
+ import { getWebFormAriaProps, combineIds, generateAccessibilityId } from '../utils/accessibility';
10
10
  import type { IdealystElement } from '../utils/refTypes';
11
11
  import { flattenStyle } from '../utils/flattenStyle';
12
12
 
@@ -31,6 +31,9 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
31
31
  size = 'md',
32
32
  type = 'outlined',
33
33
  hasError = false,
34
+ error,
35
+ helperText,
36
+ label,
34
37
  // Spacing variants from FormInputStyleProps
35
38
  margin,
36
39
  marginVertical,
@@ -65,6 +68,18 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
65
68
  const isPasswordField = inputMode === 'password' || secureTextEntry;
66
69
  const shouldShowPasswordToggle = isPasswordField && (showPasswordToggle !== false);
67
70
 
71
+ // Derive hasError from error prop or hasError boolean
72
+ const computedHasError = Boolean(error) || hasError;
73
+
74
+ // Determine if we need a wrapper (when label, error, or helperText is present)
75
+ const needsWrapper = Boolean(label) || Boolean(error) || Boolean(helperText);
76
+
77
+ // Generate unique IDs for accessibility
78
+ const inputId = useMemo(() => id || generateAccessibilityId('textinput'), [id]);
79
+ const errorId = useMemo(() => `${inputId}-error`, [inputId]);
80
+ const helperId = useMemo(() => `${inputId}-helper`, [inputId]);
81
+ const labelId = useMemo(() => label ? `${inputId}-label` : undefined, [inputId, label]);
82
+
68
83
  // Get theme for icon sizes and colors
69
84
  const { theme } = useUnistyles();
70
85
  const iconSize = theme.sizes.input[size].iconSize;
@@ -138,7 +153,7 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
138
153
  size,
139
154
  type,
140
155
  focused: isFocused,
141
- hasError,
156
+ hasError: computedHasError,
142
157
  disabled,
143
158
  margin,
144
159
  marginVertical,
@@ -146,19 +161,34 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
146
161
  });
147
162
 
148
163
  // Get web props for all styled elements (all styles are dynamic functions)
149
- const dynamicContainerStyle = (textInputStyles.container as any)({ type, focused: isFocused, hasError, disabled });
150
- const {ref: containerStyleRef, ...containerProps} = getWebProps([dynamicContainerStyle, flattenStyle(style)]);
164
+ const dynamicContainerStyle = (textInputStyles.container as any)({ type, focused: isFocused, hasError: computedHasError, disabled });
165
+ const {ref: containerStyleRef, ...containerProps} = getWebProps([dynamicContainerStyle, !needsWrapper && flattenStyle(style)].filter(Boolean));
151
166
  const leftIconContainerProps = getWebProps([(textInputStyles.leftIconContainer as any)({})]);
152
167
  const rightIconContainerProps = getWebProps([(textInputStyles.rightIconContainer as any)({})]);
153
168
  const passwordToggleProps = getWebProps([(textInputStyles.passwordToggle as any)({})]);
154
169
 
170
+ // Wrapper, label, and footer styles
171
+ const wrapperStyleComputed = (textInputStyles.wrapper as any)({});
172
+ const labelStyleComputed = (textInputStyles.label as any)({ disabled });
173
+ const footerStyleComputed = (textInputStyles.footer as any)({});
174
+ const helperTextStyleComputed = (textInputStyles.helperText as any)({ hasError: computedHasError });
175
+
176
+ const wrapperProps = getWebProps([wrapperStyleComputed, flattenStyle(style)]);
177
+ const labelProps = getWebProps([labelStyleComputed]);
178
+ const footerProps = getWebProps([footerStyleComputed]);
179
+ const helperTextProps = getWebProps([helperTextStyleComputed]);
180
+
155
181
  // Get input props
156
182
  const inputWebProps = getWebProps([(textInputStyles.input as any)({})]);
157
183
 
158
184
  // Generate accessibility props
159
185
  const ariaProps = useMemo(() => {
160
- // Derive invalid state from hasError or explicit accessibilityInvalid
161
- const isInvalid = accessibilityInvalid ?? hasError;
186
+ // Derive invalid state from computedHasError or explicit accessibilityInvalid
187
+ const isInvalid = accessibilityInvalid ?? computedHasError;
188
+ const describedByIds = combineIds(
189
+ accessibilityDescribedBy,
190
+ error ? errorId : helperText ? helperId : undefined
191
+ );
162
192
 
163
193
  return getWebFormAriaProps({
164
194
  accessibilityLabel,
@@ -166,8 +196,8 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
166
196
  accessibilityDisabled: accessibilityDisabled ?? disabled,
167
197
  accessibilityHidden,
168
198
  accessibilityRole: accessibilityRole ?? 'textbox',
169
- accessibilityLabelledBy,
170
- accessibilityDescribedBy,
199
+ accessibilityLabelledBy: accessibilityLabelledBy ?? labelId,
200
+ accessibilityDescribedBy: describedByIds,
171
201
  accessibilityControls,
172
202
  accessibilityExpanded,
173
203
  accessibilityPressed,
@@ -175,7 +205,7 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
175
205
  accessibilityHasPopup,
176
206
  accessibilityRequired,
177
207
  accessibilityInvalid: isInvalid,
178
- accessibilityErrorMessage,
208
+ accessibilityErrorMessage: accessibilityErrorMessage ?? (error ? errorId : undefined),
179
209
  accessibilityAutoComplete,
180
210
  });
181
211
  }, [
@@ -186,7 +216,12 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
186
216
  accessibilityHidden,
187
217
  accessibilityRole,
188
218
  accessibilityLabelledBy,
219
+ labelId,
189
220
  accessibilityDescribedBy,
221
+ error,
222
+ errorId,
223
+ helperText,
224
+ helperId,
190
225
  accessibilityControls,
191
226
  accessibilityExpanded,
192
227
  accessibilityPressed,
@@ -194,7 +229,7 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
194
229
  accessibilityHasPopup,
195
230
  accessibilityRequired,
196
231
  accessibilityInvalid,
197
- hasError,
232
+ computedHasError,
198
233
  accessibilityErrorMessage,
199
234
  accessibilityAutoComplete,
200
235
  ]);
@@ -257,57 +292,134 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
257
292
 
258
293
  const containerRef = useRef<HTMLDivElement>(null);
259
294
 
260
- const handleContainerPress = (e: React.MouseEvent<HTMLDivElement>) => {
295
+ const handleContainerPress = (e: React.MouseEvent<HTMLDivElement>) => {
261
296
  e.preventDefault();
262
297
  e.stopPropagation();
263
298
  containerRef.current?.focus();
264
- }
299
+ };
265
300
 
266
301
  const mergedContainerRef = useMergeRefs(containerRef, containerStyleRef);
267
302
 
303
+ const showFooter = Boolean(error) || Boolean(helperText);
304
+
305
+ // If no wrapper needed, return flat input container
306
+ if (!needsWrapper) {
307
+ return (
308
+ <div onClick={handleContainerPress} ref={mergedContainerRef} {...containerProps} id={id} data-testid={testID}>
309
+ {/* Left Icon */}
310
+ {leftIcon && (
311
+ <span {...leftIconContainerProps}>
312
+ {renderLeftIcon()}
313
+ </span>
314
+ )}
315
+
316
+ {/* Input */}
317
+ <input
318
+ {...inputWebProps}
319
+ {...ariaProps}
320
+ id={inputId}
321
+ ref={mergedInputRef}
322
+ type={getInputType()}
323
+ value={value}
324
+ onClick={handlePress}
325
+ onChange={handleChange}
326
+ onFocus={handleFocus}
327
+ onBlur={handleBlur}
328
+ onKeyDown={handleKeyDown}
329
+ placeholder={placeholder}
330
+ disabled={disabled}
331
+ autoCapitalize={autoCapitalize}
332
+ />
333
+
334
+ {/* Right Icon or Password Toggle */}
335
+ {shouldShowPasswordToggle ? (
336
+ <button
337
+ {...passwordToggleProps}
338
+ onClick={togglePasswordVisibility}
339
+ disabled={disabled}
340
+ aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
341
+ type="button"
342
+ tabIndex={-1}
343
+ >
344
+ {renderPasswordToggleIcon()}
345
+ </button>
346
+ ) : rightIcon ? (
347
+ <span {...rightIconContainerProps}>
348
+ {renderRightIcon()}
349
+ </span>
350
+ ) : null}
351
+ </div>
352
+ );
353
+ }
354
+
355
+ // With wrapper for label/error/helperText
268
356
  return (
269
- <div onClick={handleContainerPress} ref={mergedContainerRef} {...containerProps} id={id} data-testid={testID}>
270
- {/* Left Icon */}
271
- {leftIcon && (
272
- <span {...leftIconContainerProps}>
273
- {renderLeftIcon()}
274
- </span>
357
+ <div {...wrapperProps} id={id} data-testid={testID}>
358
+ {label && (
359
+ <label {...labelProps} id={labelId} htmlFor={inputId}>{label}</label>
275
360
  )}
276
361
 
277
- {/* Input */}
278
- <input
279
- {...inputWebProps}
280
- {...ariaProps}
281
- ref={mergedInputRef}
282
- type={getInputType()}
283
- value={value}
284
- onClick={handlePress}
285
- onChange={handleChange}
286
- onFocus={handleFocus}
287
- onBlur={handleBlur}
288
- onKeyDown={handleKeyDown}
289
- placeholder={placeholder}
290
- disabled={disabled}
291
- autoCapitalize={autoCapitalize}
292
- />
293
-
294
- {/* Right Icon or Password Toggle */}
295
- {shouldShowPasswordToggle ? (
296
- <button
297
- {...passwordToggleProps}
298
- onClick={togglePasswordVisibility}
362
+ <div onClick={handleContainerPress} ref={mergedContainerRef} {...containerProps}>
363
+ {/* Left Icon */}
364
+ {leftIcon && (
365
+ <span {...leftIconContainerProps}>
366
+ {renderLeftIcon()}
367
+ </span>
368
+ )}
369
+
370
+ {/* Input */}
371
+ <input
372
+ {...inputWebProps}
373
+ {...ariaProps}
374
+ id={inputId}
375
+ ref={mergedInputRef}
376
+ type={getInputType()}
377
+ value={value}
378
+ onClick={handlePress}
379
+ onChange={handleChange}
380
+ onFocus={handleFocus}
381
+ onBlur={handleBlur}
382
+ onKeyDown={handleKeyDown}
383
+ placeholder={placeholder}
299
384
  disabled={disabled}
300
- aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
301
- type="button"
302
- tabIndex={-1}
303
- >
304
- {renderPasswordToggleIcon()}
305
- </button>
306
- ) : rightIcon ? (
307
- <span {...rightIconContainerProps}>
308
- {renderRightIcon()}
309
- </span>
310
- ) : null}
385
+ autoCapitalize={autoCapitalize}
386
+ />
387
+
388
+ {/* Right Icon or Password Toggle */}
389
+ {shouldShowPasswordToggle ? (
390
+ <button
391
+ {...passwordToggleProps}
392
+ onClick={togglePasswordVisibility}
393
+ disabled={disabled}
394
+ aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
395
+ type="button"
396
+ tabIndex={-1}
397
+ >
398
+ {renderPasswordToggleIcon()}
399
+ </button>
400
+ ) : rightIcon ? (
401
+ <span {...rightIconContainerProps}>
402
+ {renderRightIcon()}
403
+ </span>
404
+ ) : null}
405
+ </div>
406
+
407
+ {showFooter && (
408
+ <div {...footerProps}>
409
+ <div style={{ flex: 1 }}>
410
+ {error && (
411
+ <span {...helperTextProps} id={errorId} role="alert">
412
+ {error}
413
+ </span>
414
+ )}
415
+ {!error && helperText && (
416
+ <span {...helperTextProps} id={helperId}>
417
+ {helperText}
418
+ </span>
419
+ )}
420
+ </div>
421
+ </div>
422
+ )}
311
423
  </div>
312
424
  );
313
425
  });
@@ -167,10 +167,25 @@ export interface TextInputProps extends FormInputStyleProps, FormAccessibilityPr
167
167
 
168
168
  /**
169
169
  * Whether the input has an error state
170
- * @deprecated Use intent="danger" instead
170
+ * @deprecated Use error prop or intent="danger" instead
171
171
  */
172
172
  hasError?: boolean;
173
173
 
174
+ /**
175
+ * Error message to display below the input. When set, the input shows error styling.
176
+ */
177
+ error?: string;
178
+
179
+ /**
180
+ * Helper text to display below the input. Hidden when error is set.
181
+ */
182
+ helperText?: string;
183
+
184
+ /**
185
+ * Label text to display above the input
186
+ */
187
+ label?: string;
188
+
174
189
  /**
175
190
  * Called when the user submits the input (presses Enter on web, or the return key on mobile).
176
191
  * Use this for form submission or moving to the next field.
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useEffect } from 'react';
2
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
2
+ import { useSafeAreaInsets } from '@idealyst/theme';
3
3
  import { calculateSmartPosition, type Placement } from '../utils/positionUtils.native';
4
4
 
5
5
  export interface UseSmartPositionOptions {
@@ -119,6 +119,9 @@ export * from './Platform/types';
119
119
  export { Keyboard } from './Keyboard';
120
120
  export * from './Keyboard/types';
121
121
 
122
+ export { default as Form, useForm } from './Form';
123
+ export * from './Form/types';
124
+
122
125
  export type { ButtonProps } from './Button/types';
123
126
  export type { IconButtonProps } from './IconButton/types';
124
127
  export type { TextProps } from './Text/types';
@@ -165,6 +168,22 @@ export type {
165
168
  KeyboardSubscription,
166
169
  KeyboardStatic,
167
170
  } from './Keyboard/types';
171
+ export type {
172
+ FormProps,
173
+ UseFormOptions,
174
+ UseFormReturn,
175
+ FormValues,
176
+ FormErrors,
177
+ FormTextInputProps,
178
+ FormTextAreaProps,
179
+ FormSelectProps,
180
+ FormCheckboxProps,
181
+ FormRadioGroupProps,
182
+ FormSwitchProps,
183
+ FormSliderProps,
184
+ FormFieldProps,
185
+ FieldRenderProps,
186
+ } from './Form/types';
168
187
 
169
188
  // Event utilities
170
189
  export * from './utils/events';
package/src/index.ts CHANGED
@@ -127,6 +127,9 @@ export * from './Platform/types';
127
127
  export { Keyboard } from './Keyboard';
128
128
  export * from './Keyboard/types';
129
129
 
130
+ export { default as Form, useForm } from './Form';
131
+ export * from './Form/types';
132
+
130
133
  export type { ButtonProps } from './Button/types';
131
134
  export type { IconButtonProps } from './IconButton/types';
132
135
  export type { TextProps } from './Text/types';
@@ -174,6 +177,22 @@ export type {
174
177
  KeyboardSubscription,
175
178
  KeyboardStatic,
176
179
  } from './Keyboard/types';
180
+ export type {
181
+ FormProps,
182
+ UseFormOptions,
183
+ UseFormReturn,
184
+ FormValues,
185
+ FormErrors,
186
+ FormTextInputProps,
187
+ FormTextAreaProps,
188
+ FormSelectProps,
189
+ FormCheckboxProps,
190
+ FormRadioGroupProps,
191
+ FormSwitchProps,
192
+ FormSliderProps,
193
+ FormFieldProps,
194
+ FieldRenderProps,
195
+ } from './Form/types';
177
196
 
178
197
  export { useMergeRefs };
179
198
 
@@ -1,6 +1,6 @@
1
1
  import React, { ReactNode } from 'react';
2
2
  import { View, Dimensions, StyleProp, ViewStyle } from 'react-native';
3
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
3
+ import { useSafeAreaInsets } from '@idealyst/theme';
4
4
 
5
5
  interface BoundedModalContentProps {
6
6
  children: ReactNode;
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { View, Text, Dimensions, StyleSheet } from 'react-native';
3
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
3
+ import { useSafeAreaInsets } from '@idealyst/theme';
4
4
 
5
5
  interface SafeAreaDebugOverlayProps {
6
6
  visible?: boolean;