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

Sign up to get free protection for your applications and to get access to all the features.
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 {