@saas-ui/forms 2.0.0-next.2 → 2.0.0-next.21

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 (62) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +53 -6
  3. package/dist/ajv/index.d.ts +24 -11
  4. package/dist/ajv/index.js +7 -9
  5. package/dist/ajv/index.js.map +1 -1
  6. package/dist/ajv/index.mjs +7 -10
  7. package/dist/ajv/index.mjs.map +1 -1
  8. package/dist/index.d.ts +519 -280
  9. package/dist/index.js +777 -696
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +756 -676
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/yup/index.d.ts +525 -21
  14. package/dist/yup/index.js +21 -9
  15. package/dist/yup/index.js.map +1 -1
  16. package/dist/yup/index.mjs +21 -10
  17. package/dist/yup/index.mjs.map +1 -1
  18. package/dist/zod/index.d.ts +525 -12
  19. package/dist/zod/index.js +21 -1
  20. package/dist/zod/index.js.map +1 -1
  21. package/dist/zod/index.mjs +21 -3
  22. package/dist/zod/index.mjs.map +1 -1
  23. package/package.json +33 -10
  24. package/src/array-field.tsx +88 -48
  25. package/src/auto-form.tsx +7 -3
  26. package/src/base-field.tsx +54 -0
  27. package/src/create-field.tsx +144 -0
  28. package/src/create-form.tsx +68 -0
  29. package/src/create-step-form.tsx +100 -0
  30. package/src/default-fields.tsx +163 -0
  31. package/src/display-field.tsx +9 -11
  32. package/src/display-if.tsx +20 -13
  33. package/src/field-resolver.ts +10 -8
  34. package/src/field.tsx +18 -445
  35. package/src/fields-context.tsx +23 -0
  36. package/src/fields.tsx +34 -21
  37. package/src/form-context.tsx +84 -0
  38. package/src/form.tsx +77 -55
  39. package/src/index.ts +58 -4
  40. package/src/input-right-button/input-right-button.stories.tsx +1 -1
  41. package/src/input-right-button/input-right-button.tsx +0 -2
  42. package/src/layout.tsx +16 -11
  43. package/src/number-input/number-input.tsx +9 -5
  44. package/src/object-field.tsx +35 -13
  45. package/src/password-input/password-input.stories.tsx +23 -2
  46. package/src/password-input/password-input.tsx +6 -6
  47. package/src/pin-input/pin-input.tsx +1 -5
  48. package/src/radio/radio-input.stories.tsx +1 -1
  49. package/src/radio/radio-input.tsx +12 -10
  50. package/src/select/native-select.tsx +1 -4
  51. package/src/select/select-context.tsx +130 -0
  52. package/src/select/select.stories.tsx +116 -85
  53. package/src/select/select.test.tsx +1 -1
  54. package/src/select/select.tsx +162 -146
  55. package/src/step-form.tsx +76 -76
  56. package/src/submit-button.tsx +5 -1
  57. package/src/types.ts +149 -0
  58. package/src/use-array-field.tsx +9 -3
  59. package/src/use-step-form.tsx +54 -9
  60. package/src/utils.ts +23 -1
  61. package/src/watch-field.tsx +2 -6
  62. /package/src/radio/{radio.test.tsx → radio-input.test.tsx} +0 -0
@@ -10,176 +10,192 @@ import {
10
10
  MenuListProps,
11
11
  MenuItemOption,
12
12
  MenuOptionGroup,
13
- MenuOptionGroupProps,
14
13
  Button,
15
14
  ButtonProps,
16
15
  omitThemingProps,
17
16
  useMultiStyleConfig,
18
17
  SystemStyleObject,
19
- useFormControl,
20
- HTMLChakraProps,
18
+ MenuItemOptionProps,
19
+ useFormControlContext,
21
20
  } from '@chakra-ui/react'
22
- import { ChevronDownIcon } from '@chakra-ui/icons'
23
- import { cx, __DEV__ } from '@chakra-ui/utils'
21
+ import { cx, dataAttr } from '@chakra-ui/utils'
22
+ import { ChevronDownIcon } from '@saas-ui/core'
24
23
 
25
- interface Option {
26
- value: string
27
- label?: string
28
- }
24
+ import { FieldOption } from '../types'
29
25
 
30
- interface SelectOptions {
31
- /**
32
- * An array of options
33
- * If you leave this empty the children prop will be rendered.
34
- */
35
- options?: Option[]
36
- /**
37
- * Props passed to the MenuList.
38
- */
39
- menuListProps?: MenuListProps
40
- /**
41
- * Customize how the value is rendered.
42
- * @type (value?: string[]) => React.ReactElement
43
- */
44
- renderValue?: (value?: string[]) => React.ReactElement | undefined
45
- /**
46
- * Enable multiple select.
47
- */
48
- multiple?: boolean
49
- }
26
+ import {
27
+ SelectOptions,
28
+ SelectProvider,
29
+ useSelect,
30
+ useSelectContext,
31
+ } from './select-context'
32
+
33
+ export interface SelectOption
34
+ extends Omit<MenuItemOptionProps, 'value'>,
35
+ FieldOption {}
50
36
 
51
37
  export interface SelectProps
52
38
  extends Omit<MenuProps, 'children'>,
53
- Pick<ButtonProps, 'isDisabled' | 'leftIcon' | 'rightIcon'>,
54
- Pick<MenuOptionGroupProps, 'onChange'>,
55
39
  SelectOptions {}
56
40
 
57
- const SelectButton = forwardRef((props, ref) => {
58
- const styles = useMultiStyleConfig('Input', props)
59
-
60
- /* @ts-ignore */
61
- const focusStyles = styles.field._focusVisible
62
-
63
- const height = styles.field.h || styles.field.height
64
-
65
- const buttonStyles: SystemStyleObject = {
66
- fontWeight: 'normal',
67
- textAlign: 'left',
68
- color: 'inherit',
69
- _active: {
70
- bg: 'transparent',
71
- },
72
- minH: height,
73
- _focus: focusStyles,
74
- _expanded: focusStyles,
75
- ...styles.field,
76
- h: 'auto',
41
+ export interface SelectButtonProps extends ButtonProps {}
42
+
43
+ /**
44
+ * Button that opens the select menu and displays the selected value.
45
+ *
46
+ * @see https://saas-ui.dev/docs/components/forms/select
47
+ */
48
+ export const SelectButton = forwardRef<SelectButtonProps, 'button'>(
49
+ (props, ref) => {
50
+ const styles = useMultiStyleConfig('SuiSelect', props)
51
+
52
+ const {
53
+ displayValue,
54
+ renderValue,
55
+ placeholder,
56
+ isDisabled: isSelectDisabled,
57
+ } = useSelectContext()
58
+
59
+ const context = useFormControlContext()
60
+
61
+ const {
62
+ isInvalid,
63
+ isReadOnly,
64
+ isDisabled,
65
+ isFocused,
66
+ isRequired,
67
+ id,
68
+ onBlur,
69
+ onFocus,
70
+ } = context || {}
71
+
72
+ const { rightIcon = <ChevronDownIcon />, ...rest } = props
73
+
74
+ /* @ts-ignore */
75
+ const focusStyles = styles.field?._focusVisible
76
+ /* @ts-ignore */
77
+ const readOnlyStyles = styles.field?._readOnly
78
+ /* @ts-ignore */
79
+ const invalid = styles.field?._invalid
80
+
81
+ const height = styles.field?.h || styles.field?.height
82
+
83
+ const buttonStyles: any = {
84
+ fontWeight: 'normal',
85
+ textAlign: 'left',
86
+ color: 'inherit',
87
+ _active: {
88
+ bg: 'transparent',
89
+ },
90
+ minH: height,
91
+ _focus: focusStyles,
92
+ _expanded: focusStyles,
93
+ _readOnly: readOnlyStyles,
94
+ _invalid: invalid,
95
+ ...styles.field,
96
+ h: 'auto',
97
+ }
98
+
99
+ // Using a Button, so we can simply use leftIcon and rightIcon
100
+ return (
101
+ <MenuButton
102
+ as={Button}
103
+ id={id || React.useId()}
104
+ {...buttonStyles}
105
+ {...rest}
106
+ onFocus={onFocus}
107
+ onBlur={onBlur}
108
+ isDisabled={isDisabled || isSelectDisabled}
109
+ data-invalid={dataAttr(isInvalid)}
110
+ data-read-only={dataAttr(isReadOnly)}
111
+ data-focus={dataAttr(isFocused)}
112
+ data-required={dataAttr(isRequired)}
113
+ rightIcon={rightIcon}
114
+ ref={ref}
115
+ >
116
+ {renderValue(displayValue) || placeholder}
117
+ </MenuButton>
118
+ )
77
119
  }
120
+ )
78
121
 
79
- // Using a Button, so we can simply use leftIcon and rightIcon
80
- return <MenuButton as={Button} {...props} ref={ref} sx={buttonStyles} />
81
- })
82
-
83
- if (__DEV__) {
84
- SelectButton.displayName = 'SelectButton'
85
- }
122
+ SelectButton.displayName = 'SelectButton'
86
123
 
124
+ /**
125
+ * Allow users to select a value from a list of options.
126
+ *
127
+ * @see https://saas-ui.dev/docs/components/forms/select
128
+ */
87
129
  export const Select = forwardRef<SelectProps, 'select'>((props, ref) => {
88
- const {
89
- name,
90
- options,
91
- children,
92
- onChange,
93
- defaultValue,
94
- value,
95
- placeholder,
96
- isDisabled,
97
- leftIcon,
98
- rightIcon = <ChevronDownIcon />,
99
- multiple,
100
- size,
101
- variant,
102
- menuListProps,
103
- renderValue = (value) => value?.join(', '),
104
- ...rest
105
- } = props
106
- const menuProps = omitThemingProps(rest)
130
+ const { name, children, isDisabled, multiple, ...rest } = props
107
131
 
108
- const [currentValue, setCurrentValue] = React.useState(value || defaultValue)
109
-
110
- const controlProps = useFormControl({ name } as HTMLChakraProps<'input'>)
132
+ const menuProps = omitThemingProps(rest)
111
133
 
112
- const handleChange = (value: string | string[]) => {
113
- setCurrentValue(value)
114
- onChange?.(value)
115
- }
134
+ const context = useSelect(props)
116
135
 
117
- const buttonProps = {
118
- isDisabled,
119
- leftIcon,
120
- rightIcon,
121
- size,
122
- variant,
123
- }
136
+ const { value, controlProps } = context
124
137
 
125
- const getDisplayValue = React.useCallback(
126
- (value: string) => {
127
- if (!options) {
128
- return value
129
- }
130
-
131
- for (const option of options) {
132
- if (option.label && option.value === value) {
133
- return option.label
134
- }
135
- }
136
-
137
- return value
138
- },
139
- [options]
138
+ return (
139
+ <SelectProvider value={context}>
140
+ <Menu {...menuProps} closeOnSelect={!multiple}>
141
+ <chakra.div className={cx('sui-select')}>
142
+ {children}
143
+ <chakra.input
144
+ {...controlProps}
145
+ ref={ref}
146
+ name={name}
147
+ type="hidden"
148
+ value={value || ''}
149
+ className="saas-select__input"
150
+ />
151
+ </chakra.div>
152
+ </Menu>
153
+ </SelectProvider>
140
154
  )
155
+ })
156
+
157
+ export interface SelectListProps extends MenuListProps {}
141
158
 
142
- const displayValue = currentValue
143
- ? (Array.isArray(currentValue) ? currentValue : [currentValue]).map(
144
- getDisplayValue
145
- )
146
- : []
159
+ /**
160
+ * The list of options to choose from.
161
+ *
162
+ * @see https://saas-ui.dev/docs/components/forms/select
163
+ */
164
+ export const SelectList: React.FC<SelectListProps> = (props) => {
165
+ const { defaultValue, value, options, multiple, onChange } =
166
+ useSelectContext()
147
167
 
148
168
  return (
149
- <Menu {...menuProps} closeOnSelect={!multiple}>
150
- <chakra.div className={cx('sui-select')}>
151
- <SelectButton ref={ref} {...buttonProps}>
152
- {renderValue(displayValue) || placeholder}
153
- </SelectButton>
154
- <MenuList maxH="60vh" overflowY="auto" {...menuListProps}>
155
- <MenuOptionGroup
156
- defaultValue={
157
- (defaultValue || value) as string | string[] | undefined
158
- }
159
- onChange={handleChange}
160
- type={multiple ? 'checkbox' : 'radio'}
161
- >
162
- {options
163
- ? options.map(({ value, label, ...rest }, i) => (
164
- <MenuItemOption key={i} value={value} {...rest}>
165
- {label || value}
166
- </MenuItemOption>
167
- ))
168
- : children}
169
- </MenuOptionGroup>
170
- </MenuList>
171
- <chakra.input
172
- {...controlProps}
173
- name={name}
174
- type="hidden"
175
- value={currentValue}
176
- className="saas-select__input"
177
- />
178
- </chakra.div>
179
- </Menu>
169
+ <MenuList maxH="100vh" overflowY="auto" {...props}>
170
+ <MenuOptionGroup
171
+ defaultValue={(defaultValue || value) as string | string[] | undefined}
172
+ value={value}
173
+ onChange={onChange}
174
+ type={multiple ? 'checkbox' : 'radio'}
175
+ >
176
+ {options
177
+ ? options.map(({ value, label, ...rest }, i) => (
178
+ <SelectOption key={i} value={value} {...rest}>
179
+ {label || value}
180
+ </SelectOption>
181
+ ))
182
+ : props.children}
183
+ </MenuOptionGroup>
184
+ </MenuList>
180
185
  )
181
- })
182
-
183
- if (__DEV__) {
184
- Select.displayName = 'Select'
185
186
  }
187
+
188
+ Select.displayName = 'Select'
189
+
190
+ /**
191
+ * An option in a select list
192
+ *
193
+ * @see https://saas-ui.dev/docs/components/forms/select
194
+ */
195
+ export const SelectOption = forwardRef<MenuItemOptionProps, 'button'>(
196
+ (props, ref) => {
197
+ return <MenuItemOption ref={ref} {...props} />
198
+ }
199
+ )
200
+ SelectOption.id = 'MenuItemOption'
201
+ SelectOption.displayName = 'SelectOption'
package/src/step-form.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react'
2
2
 
3
- import { FieldValues, UseFormReturn } from 'react-hook-form'
3
+ import { FieldValues } from 'react-hook-form'
4
4
 
5
5
  import {
6
6
  chakra,
@@ -10,71 +10,48 @@ import {
10
10
  ThemingProps,
11
11
  } from '@chakra-ui/react'
12
12
 
13
- import { callAllHandlers, runIfFn, cx, __DEV__ } from '@chakra-ui/utils'
13
+ import { callAllHandlers, cx } from '@chakra-ui/utils'
14
14
 
15
15
  import {
16
- StepperProvider,
17
- StepperSteps,
18
- StepperStepsProps,
19
- StepperStep,
16
+ Steps,
17
+ StepsItem,
18
+ StepsItemProps,
19
+ StepsProps,
20
20
  useStepperContext,
21
- StepperContainer,
22
- StepperProps,
23
21
  } from '@saas-ui/core'
24
22
 
25
- import { Form } from './form'
26
23
  import { SubmitButton } from './submit-button'
27
24
 
28
25
  import {
29
- useStepForm,
30
26
  useFormStep,
31
- StepFormProvider,
32
27
  UseStepFormProps,
33
28
  FormStepSubmitHandler,
34
29
  } from './use-step-form'
30
+ import { FieldProps } from './types'
31
+
32
+ export type StepsOptions<TSchema, TName extends string = string> = {
33
+ /**
34
+ * The step name
35
+ */
36
+ name: TName
37
+ /**
38
+ * Schema
39
+ */
40
+ schema?: TSchema
41
+ }[]
35
42
 
36
43
  export interface StepFormProps<
44
+ TSteps extends StepsOptions<any> = StepsOptions<any>,
37
45
  TFieldValues extends FieldValues = FieldValues,
38
- TContext extends object = object
39
- > extends UseStepFormProps<TFieldValues> {}
40
-
41
- export const StepForm = React.forwardRef(
42
- <
43
- TFieldValues extends FieldValues = FieldValues,
44
- TContext extends object = object
45
- >(
46
- props: StepFormProps<TFieldValues, TContext>,
47
- ref: React.ForwardedRef<HTMLFormElement>
48
- ) => {
49
- const { children, ...rest } = props
50
-
51
- const stepper = useStepForm<TFieldValues>(props)
52
-
53
- const { getFormProps, ...ctx } = stepper
54
-
55
- const context = React.useMemo(() => ctx, [ctx])
56
-
57
- return (
58
- <StepperProvider value={context}>
59
- <StepFormProvider value={context}>
60
- <Form ref={ref} {...rest} {...getFormProps()}>
61
- {runIfFn(children, stepper)}
62
- </Form>
63
- </StepFormProvider>
64
- </StepperProvider>
65
- )
66
- }
67
- ) as <TFieldValues extends FieldValues>(
68
- props: StepFormProps<TFieldValues> & {
69
- ref?: React.ForwardedRef<HTMLFormElement>
70
- }
71
- ) => React.ReactElement
72
-
73
- export interface FormStepOptions {
46
+ TContext extends object = object,
47
+ TFieldTypes = FieldProps<TFieldValues>
48
+ > extends UseStepFormProps<TSteps, TFieldValues, TContext, TFieldTypes> {}
49
+
50
+ export interface FormStepOptions<TName extends string = string> {
74
51
  /**
75
52
  * The step name
76
53
  */
77
- name: string
54
+ name: TName
78
55
  /**
79
56
  * Schema
80
57
  */
@@ -85,14 +62,28 @@ export interface FormStepOptions {
85
62
  resolver?: any
86
63
  }
87
64
 
88
- export interface FormStepperProps
89
- extends StepperStepsProps,
90
- ThemingProps<'Stepper'> {}
65
+ export interface FormStepperProps extends StepsProps, ThemingProps<'Stepper'> {
66
+ render?: StepsItemProps['render']
67
+ }
91
68
 
69
+ /**
70
+ * Renders a stepper that displays progress above the form.
71
+ *
72
+ * @see Docs https://saas-ui.dev/docs/components/forms/step-form
73
+ */
92
74
  export const FormStepper: React.FC<FormStepperProps> = (props) => {
93
75
  const { activeIndex, setIndex } = useStepperContext()
94
76
 
95
- const { children, orientation, variant, colorScheme, size, ...rest } = props
77
+ const {
78
+ children,
79
+ orientation,
80
+ variant,
81
+ colorScheme,
82
+ size,
83
+ onChange: onChangeProp,
84
+ render,
85
+ ...rest
86
+ } = props
96
87
 
97
88
  const elements = React.Children.map(children, (child) => {
98
89
  if (
@@ -101,14 +92,15 @@ export const FormStepper: React.FC<FormStepperProps> = (props) => {
101
92
  ) {
102
93
  const { isCompleted } = useFormStep(child.props) // Register this step
103
94
  return (
104
- <StepperStep
95
+ <StepsItem
96
+ render={render}
105
97
  name={child.props.name}
106
98
  title={child.props.title}
107
99
  isCompleted={isCompleted}
108
100
  {...rest}
109
101
  >
110
102
  {child.props.children}
111
- </StepperStep>
103
+ </StepsItem>
112
104
  )
113
105
  }
114
106
  return child
@@ -116,10 +108,11 @@ export const FormStepper: React.FC<FormStepperProps> = (props) => {
116
108
 
117
109
  const onChange = React.useCallback((i: number) => {
118
110
  setIndex(i)
111
+ onChangeProp?.(i)
119
112
  }, [])
120
113
 
121
114
  return (
122
- <StepperContainer
115
+ <Steps
123
116
  orientation={orientation}
124
117
  step={activeIndex}
125
118
  variant={variant}
@@ -127,23 +120,26 @@ export const FormStepper: React.FC<FormStepperProps> = (props) => {
127
120
  size={size}
128
121
  onChange={onChange}
129
122
  >
130
- <StepperSteps mb="4" {...props}>
131
- {elements}
132
- </StepperSteps>
133
- </StepperContainer>
123
+ {elements}
124
+ </Steps>
134
125
  )
135
126
  }
136
127
 
137
- export interface FormStepProps
138
- extends FormStepOptions,
128
+ export interface FormStepProps<TName extends string = string>
129
+ extends FormStepOptions<TName>,
139
130
  Omit<HTMLChakraProps<'div'>, 'onSubmit'> {
140
131
  onSubmit?: FormStepSubmitHandler
141
132
  }
142
-
143
- export const FormStep: React.FC<FormStepProps> = (props) => {
144
- const { name, schema, resolver, children, className, onSubmit, ...rest } =
145
- props
146
- const step = useFormStep({ name, schema, resolver, onSubmit })
133
+ /**
134
+ * The form step containing fields for a specific step.
135
+ *
136
+ * @see Docs https://saas-ui.dev/docs/components/forms/step-form
137
+ */
138
+ export const FormStep = <TName extends string = string>(
139
+ props: FormStepProps<TName>
140
+ ) => {
141
+ const { name, children, className, onSubmit, ...rest } = props
142
+ const step = useFormStep({ name, onSubmit })
147
143
 
148
144
  const { isActive } = step
149
145
 
@@ -154,17 +150,20 @@ export const FormStep: React.FC<FormStepProps> = (props) => {
154
150
  ) : null
155
151
  }
156
152
 
157
- if (__DEV__) {
158
- FormStep.displayName = 'FormStep'
159
- }
153
+ FormStep.displayName = 'FormStep'
160
154
 
155
+ /**
156
+ * A button that this opens the previous step when clicked. Disabled on the first step.
157
+ *
158
+ * @see Docs https://saas-ui.dev/docs/components/forms/step-form
159
+ */
161
160
  export const PrevButton: React.FC<ButtonProps> = (props) => {
162
161
  const { isFirstStep, isCompleted, prevStep } = useStepperContext()
163
162
 
164
163
  return (
165
164
  <Button
166
165
  isDisabled={isFirstStep || isCompleted}
167
- label="Back"
166
+ children="Back"
168
167
  {...props}
169
168
  className={cx('sui-form__prev-button', props.className)}
170
169
  onClick={callAllHandlers(props.onClick, prevStep)}
@@ -172,15 +171,18 @@ export const PrevButton: React.FC<ButtonProps> = (props) => {
172
171
  )
173
172
  }
174
173
 
175
- if (__DEV__) {
176
- PrevButton.displayName = 'PrevButton'
177
- }
174
+ PrevButton.displayName = 'PrevButton'
178
175
 
179
176
  export interface NextButtonProps extends Omit<ButtonProps, 'children'> {
180
177
  submitLabel?: string
181
178
  label?: string
182
179
  }
183
180
 
181
+ /**
182
+ * A button that submits the active step.
183
+ *
184
+ * @see Docs https://saas-ui.dev/docs/components/forms/step-form
185
+ */
184
186
  export const NextButton: React.FC<NextButtonProps> = (props) => {
185
187
  const { label = 'Next', submitLabel = 'Complete', ...rest } = props
186
188
  const { isLastStep, isCompleted } = useStepperContext()
@@ -196,6 +198,4 @@ export const NextButton: React.FC<NextButtonProps> = (props) => {
196
198
  )
197
199
  }
198
200
 
199
- if (__DEV__) {
200
- NextButton.displayName = 'NextButton'
201
- }
201
+ NextButton.displayName = 'NextButton'
@@ -20,7 +20,11 @@ export interface SubmitButtonProps extends ButtonProps {
20
20
  */
21
21
  disableIfInvalid?: boolean
22
22
  }
23
-
23
+ /**
24
+ * A button with type submit and default color scheme primary and isLoading state when the form is submitting.
25
+ *
26
+ * @see Docs https://saas-ui.dev/docs/components/forms/form
27
+ */
24
28
  export const SubmitButton = forwardRef<SubmitButtonProps, 'button'>(
25
29
  (props, ref) => {
26
30
  const {