@saas-ui/forms 2.0.0-next.5 → 2.0.0-next.7

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 (44) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/ajv/index.d.ts +358 -11
  3. package/dist/ajv/index.js +7 -9
  4. package/dist/ajv/index.js.map +1 -1
  5. package/dist/ajv/index.mjs +7 -10
  6. package/dist/ajv/index.mjs.map +1 -1
  7. package/dist/index.d.ts +296 -194
  8. package/dist/index.js +345 -2584
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +347 -2580
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/yup/index.d.ts +573 -106
  13. package/dist/yup/index.js +6 -10
  14. package/dist/yup/index.js.map +1 -1
  15. package/dist/yup/index.mjs +4 -8
  16. package/dist/yup/index.mjs.map +1 -1
  17. package/dist/zod/index.d.ts +490 -14
  18. package/dist/zod/index.js +5 -0
  19. package/dist/zod/index.js.map +1 -1
  20. package/dist/zod/index.mjs +5 -1
  21. package/dist/zod/index.mjs.map +1 -1
  22. package/package.json +16 -9
  23. package/src/array-field.tsx +34 -17
  24. package/src/base-field.tsx +4 -9
  25. package/src/create-field.tsx +2 -1
  26. package/src/create-form.tsx +33 -10
  27. package/src/default-fields.tsx +21 -4
  28. package/src/display-field.tsx +1 -2
  29. package/src/display-if.tsx +14 -8
  30. package/src/field-resolver.ts +10 -8
  31. package/src/field.tsx +6 -3
  32. package/src/fields.tsx +16 -13
  33. package/src/form-context.tsx +84 -0
  34. package/src/form.tsx +44 -17
  35. package/src/index.ts +17 -15
  36. package/src/object-field.tsx +6 -2
  37. package/src/password-input/password-input.tsx +1 -1
  38. package/src/select/select-context.tsx +130 -0
  39. package/src/select/select.stories.tsx +116 -85
  40. package/src/select/select.tsx +154 -142
  41. package/src/types.ts +59 -6
  42. package/src/use-array-field.tsx +9 -3
  43. package/src/utils.ts +8 -1
  44. package/src/watch-field.tsx +2 -6
@@ -10,180 +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,
21
18
  MenuItemOptionProps,
19
+ useFormControlContext,
22
20
  } from '@chakra-ui/react'
23
- import { cx } from '@chakra-ui/utils'
21
+ import { cx, dataAttr } from '@chakra-ui/utils'
24
22
  import { ChevronDownIcon } from '@saas-ui/core'
25
23
 
26
- import { FieldOptions, FieldOption } from '../types'
27
- import { mapOptions } from '../utils'
24
+ import { FieldOption } from '../types'
25
+
26
+ import {
27
+ SelectOptions,
28
+ SelectProvider,
29
+ useSelect,
30
+ useSelectContext,
31
+ } from './select-context'
28
32
 
29
33
  export interface SelectOption
30
34
  extends Omit<MenuItemOptionProps, 'value'>,
31
35
  FieldOption {}
32
36
 
33
- interface SelectOptions {
34
- /**
35
- * An array of options
36
- * If you leave this empty the children prop will be rendered.
37
- */
38
- options?: FieldOptions<SelectOption>
39
- /**
40
- * Props passed to the MenuList.
41
- */
42
- menuListProps?: MenuListProps
43
- /**
44
- * Customize how the value is rendered.
45
- * @type (value?: string[]) => React.ReactElement
46
- */
47
- renderValue?: (value?: string[]) => React.ReactElement | undefined
48
- /**
49
- * Enable multiple select.
50
- */
51
- multiple?: boolean
52
- }
53
-
54
37
  export interface SelectProps
55
38
  extends Omit<MenuProps, 'children'>,
56
- Pick<ButtonProps, 'isDisabled' | 'leftIcon' | 'rightIcon'>,
57
- Pick<MenuOptionGroupProps, 'onChange'>,
58
39
  SelectOptions {}
59
40
 
60
- const SelectButton = forwardRef((props, ref) => {
61
- const styles = useMultiStyleConfig('Input', props)
62
-
63
- /* @ts-ignore */
64
- const focusStyles = styles.field._focusVisible
65
-
66
- const height = styles.field.h || styles.field.height
67
-
68
- const buttonStyles: SystemStyleObject = {
69
- fontWeight: 'normal',
70
- textAlign: 'left',
71
- color: 'inherit',
72
- _active: {
73
- bg: 'transparent',
74
- },
75
- minH: height,
76
- _focus: focusStyles,
77
- _expanded: focusStyles,
78
- ...styles.field,
79
- 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: SystemStyleObject = {
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
+ {...rest}
105
+ onFocus={onFocus}
106
+ onBlur={onBlur}
107
+ isDisabled={isDisabled || isSelectDisabled}
108
+ data-invalid={dataAttr(isInvalid)}
109
+ data-read-only={dataAttr(isReadOnly)}
110
+ data-focus={dataAttr(isFocused)}
111
+ data-required={dataAttr(isRequired)}
112
+ rightIcon={rightIcon}
113
+ ref={ref}
114
+ sx={buttonStyles}
115
+ >
116
+ {renderValue(displayValue) || placeholder}
117
+ </MenuButton>
118
+ )
80
119
  }
81
-
82
- // Using a Button, so we can simply use leftIcon and rightIcon
83
- return <MenuButton as={Button} {...props} ref={ref} sx={buttonStyles} />
84
- })
120
+ )
85
121
 
86
122
  SelectButton.displayName = 'SelectButton'
87
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
+ */
88
129
  export const Select = forwardRef<SelectProps, 'select'>((props, ref) => {
89
- const {
90
- name,
91
- options: optionsProp,
92
- children,
93
- onChange,
94
- defaultValue,
95
- value,
96
- placeholder,
97
- isDisabled,
98
- leftIcon,
99
- rightIcon = <ChevronDownIcon />,
100
- multiple,
101
- size,
102
- variant,
103
- menuListProps,
104
- renderValue = (value) => value?.join(', '),
105
- ...rest
106
- } = props
130
+ const { name, children, isDisabled, multiple, ...rest } = props
131
+
107
132
  const menuProps = omitThemingProps(rest)
108
133
 
109
- const [currentValue, setCurrentValue] = React.useState(value || defaultValue)
134
+ const context = useSelect(props)
110
135
 
111
- const controlProps = useFormControl({ name } as HTMLChakraProps<'input'>)
136
+ const { value, controlProps } = context
112
137
 
113
- const options = React.useMemo(
114
- () => optionsProp && mapOptions(optionsProp),
115
- [optionsProp]
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>
116
154
  )
155
+ })
117
156
 
118
- const handleChange = (value: string | string[]) => {
119
- setCurrentValue(value)
120
- onChange?.(value)
121
- }
122
-
123
- const buttonProps = {
124
- isDisabled,
125
- leftIcon,
126
- rightIcon,
127
- size,
128
- variant,
129
- }
130
-
131
- const getDisplayValue = React.useCallback(
132
- (value: string) => {
133
- if (!options) {
134
- return value
135
- }
136
-
137
- for (const option of options) {
138
- if (option.label && option.value === value) {
139
- return option.label
140
- }
141
- }
142
-
143
- return value
144
- },
145
- [options]
146
- )
157
+ export interface SelectListProps extends MenuListProps {}
147
158
 
148
- const displayValue = currentValue
149
- ? (Array.isArray(currentValue) ? currentValue : [currentValue]).map(
150
- getDisplayValue
151
- )
152
- : []
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()
153
167
 
154
168
  return (
155
- <Menu {...menuProps} closeOnSelect={!multiple}>
156
- <chakra.div className={cx('sui-select')}>
157
- <SelectButton ref={ref} {...buttonProps}>
158
- {renderValue(displayValue) || placeholder}
159
- </SelectButton>
160
- <MenuList maxH="60vh" overflowY="auto" {...menuListProps}>
161
- <MenuOptionGroup
162
- defaultValue={
163
- (defaultValue || value) as string | string[] | undefined
164
- }
165
- onChange={handleChange}
166
- type={multiple ? 'checkbox' : 'radio'}
167
- >
168
- {options
169
- ? options.map(({ value, label, ...rest }, i) => (
170
- <MenuItemOption key={i} value={value} {...rest}>
171
- {label || value}
172
- </MenuItemOption>
173
- ))
174
- : children}
175
- </MenuOptionGroup>
176
- </MenuList>
177
- <chakra.input
178
- {...controlProps}
179
- name={name}
180
- type="hidden"
181
- value={currentValue}
182
- className="saas-select__input"
183
- />
184
- </chakra.div>
185
- </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>
186
185
  )
187
- })
186
+ }
188
187
 
189
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/types.ts CHANGED
@@ -3,8 +3,9 @@ import { MaybeRenderProp } from '@chakra-ui/react-utils'
3
3
  import { FieldPath, FieldValues, RegisterOptions } from 'react-hook-form'
4
4
  import { DefaultFields } from './default-fields'
5
5
  import { FormProps, FormRenderContext } from './form'
6
+ import { SubmitButtonProps } from './submit-button'
6
7
 
7
- export type FieldOption = { label: string; value: string }
8
+ export type FieldOption = { label?: string; value: string }
8
9
  export type FieldOptions<TOption extends FieldOption = FieldOption> =
9
10
  | Array<string>
10
11
  | Array<TOption>
@@ -12,6 +13,28 @@ export type FieldOptions<TOption extends FieldOption = FieldOption> =
12
13
  export type ValueOf<T> = T[keyof T]
13
14
  export type ShallowMerge<A, B> = Omit<A, keyof B> & B
14
15
 
16
+ type Split<S extends string, D extends string> = string extends S
17
+ ? string[]
18
+ : S extends ''
19
+ ? []
20
+ : S extends `${infer T}${D}${infer U}`
21
+ ? [T, ...Split<U, D>]
22
+ : [S]
23
+
24
+ type MapPath<T extends string[]> = T extends [infer U, ...infer R]
25
+ ? U extends string
26
+ ? `${U extends `${number}` ? '$' : U}${R[0] extends string
27
+ ? '.'
28
+ : ''}${R extends string[] ? MapPath<R> : ''}`
29
+ : ''
30
+ : ''
31
+
32
+ type TransformPath<T extends string> = MapPath<Split<T, '.'>>
33
+
34
+ export type ArrayFieldPath<Name extends string> = Name extends string
35
+ ? TransformPath<Name>
36
+ : never
37
+
15
38
  export interface BaseFieldProps<
16
39
  TFieldValues extends FieldValues = FieldValues,
17
40
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
@@ -19,7 +42,7 @@ export interface BaseFieldProps<
19
42
  /**
20
43
  * The field name
21
44
  */
22
- name: TName
45
+ name: TName | ArrayFieldPath<TName>
23
46
  /**
24
47
  * The field label
25
48
  */
@@ -52,12 +75,18 @@ export interface BaseFieldProps<
52
75
  placeholder?: string
53
76
  }
54
77
 
78
+ type FieldPathWithArray<
79
+ TFieldValues extends FieldValues,
80
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
81
+ > = TName | ArrayFieldPath<TName>
82
+
55
83
  type MergeFieldProps<
56
84
  FieldDefs,
57
- TFieldValues extends FieldValues = FieldValues
85
+ TFieldValues extends FieldValues = FieldValues,
86
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
58
87
  > = ValueOf<{
59
88
  [K in keyof FieldDefs]: FieldDefs[K] extends React.FC<infer Props>
60
- ? { type?: K } & ShallowMerge<Props, BaseFieldProps<TFieldValues>>
89
+ ? { type?: K } & ShallowMerge<Props, BaseFieldProps<TFieldValues, TName>>
61
90
  : never
62
91
  }>
63
92
 
@@ -81,11 +110,35 @@ export type FormChildren<
81
110
  >
82
111
  >
83
112
 
113
+ export type DefaultFieldOverrides = {
114
+ submit?: SubmitButtonProps
115
+ [key: string]: any
116
+ }
117
+
118
+ export type FieldOverrides<
119
+ FieldDefs,
120
+ TFieldValues extends FieldValues = FieldValues,
121
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
122
+ > = {
123
+ [K in FieldPathWithArray<TFieldValues, TName>]?: Omit<
124
+ MergeFieldProps<
125
+ FieldDefs extends never
126
+ ? DefaultFields
127
+ : ShallowMerge<DefaultFields, FieldDefs>,
128
+ TFieldValues
129
+ >,
130
+ 'name'
131
+ >
132
+ }
133
+
84
134
  export type WithFields<
85
135
  TFormProps extends FormProps<any, any, any, any>,
86
136
  FieldDefs
87
137
  > = TFormProps extends FormProps<infer TFieldValues, infer TContext>
88
- ? Omit<TFormProps, 'children'> & {
89
- children: FormChildren<FieldDefs, TFieldValues, TContext>
138
+ ? Omit<TFormProps, 'children' | 'fields'> & {
139
+ children?: FormChildren<FieldDefs, TFieldValues, TContext>
140
+ fields?: FieldOverrides<FieldDefs, TFieldValues> & {
141
+ submit?: SubmitButtonProps
142
+ }
90
143
  }
91
144
  : never
@@ -1,10 +1,13 @@
1
1
  import * as React from 'react'
2
2
  import {
3
3
  useFieldArray,
4
- useFormContext,
5
4
  UseFieldArrayReturn,
5
+ FieldValues,
6
+ FieldPath,
6
7
  } from 'react-hook-form'
7
8
 
9
+ import { useFormContext } from './form-context'
10
+
8
11
  import { createContext } from '@chakra-ui/react-utils'
9
12
 
10
13
  export interface UseArrayFieldReturn extends UseFieldArrayReturn {
@@ -59,11 +62,14 @@ export const [ArrayFieldRowProvider, useArrayFieldRowContext] =
59
62
  name: 'ArrayFieldRowContext',
60
63
  })
61
64
 
62
- export interface ArrayFieldOptions {
65
+ export interface ArrayFieldOptions<
66
+ TFieldValues extends FieldValues = FieldValues,
67
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
68
+ > {
63
69
  /**
64
70
  * The field name
65
71
  */
66
- name: string
72
+ name: TName
67
73
  /**
68
74
  * Default value for new values in the array
69
75
  */
package/src/utils.ts CHANGED
@@ -4,9 +4,16 @@ import { FieldOption, FieldOptions } from './types'
4
4
  export const mapNestedFields = (name: string, children: React.ReactNode) => {
5
5
  return React.Children.map(children, (child) => {
6
6
  if (React.isValidElement(child) && child.props.name) {
7
+ let childName = child.props.name
8
+ if (childName.includes('.')) {
9
+ childName = childName.replace(/^.*\.(.*)/, '$1')
10
+ } else if (childName.includes('.$')) {
11
+ childName = childName.replace(/^.*\.\$(.*)/, '$1')
12
+ }
13
+
7
14
  return React.cloneElement(child, {
8
15
  ...child.props,
9
- name: `${name}.${child.props.name}`,
16
+ name: `${name}.${childName}`,
10
17
  })
11
18
  }
12
19
  return child
@@ -1,9 +1,5 @@
1
- import {
2
- FieldValues,
3
- useFormContext,
4
- UseFormReturn,
5
- useWatch,
6
- } from 'react-hook-form'
1
+ import { FieldValues, useWatch } from 'react-hook-form'
2
+ import { useFormContext, UseFormReturn } from './form-context'
7
3
 
8
4
  export interface WatchFieldProps<
9
5
  Value = unknown,