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

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 +13 -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 +373 -2613
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +373 -2607
  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 +15 -8
  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 +152 -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,190 @@ 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 {
60
+ isInvalid,
61
+ isReadOnly,
62
+ isDisabled,
63
+ isFocused,
64
+ isRequired,
65
+ id,
66
+ onBlur,
67
+ onFocus,
68
+ } = useFormControlContext()
69
+
70
+ const { rightIcon = <ChevronDownIcon />, ...rest } = props
71
+
72
+ /* @ts-ignore */
73
+ const focusStyles = styles.field?._focusVisible
74
+ /* @ts-ignore */
75
+ const readOnlyStyles = styles.field?._readOnly
76
+ /* @ts-ignore */
77
+ const invalid = styles.field?._invalid
78
+
79
+ const height = styles.field?.h || styles.field?.height
80
+
81
+ const buttonStyles: SystemStyleObject = {
82
+ fontWeight: 'normal',
83
+ textAlign: 'left',
84
+ color: 'inherit',
85
+ _active: {
86
+ bg: 'transparent',
87
+ },
88
+ minH: height,
89
+ _focus: focusStyles,
90
+ _expanded: focusStyles,
91
+ _readOnly: readOnlyStyles,
92
+ _invalid: invalid,
93
+ ...styles.field,
94
+ h: 'auto',
95
+ }
96
+
97
+ // Using a Button, so we can simply use leftIcon and rightIcon
98
+ return (
99
+ <MenuButton
100
+ as={Button}
101
+ id={id}
102
+ {...rest}
103
+ onFocus={onFocus}
104
+ onBlur={onBlur}
105
+ isDisabled={isDisabled || isSelectDisabled}
106
+ data-invalid={dataAttr(isInvalid)}
107
+ data-read-only={dataAttr(isReadOnly)}
108
+ data-focus={dataAttr(isFocused)}
109
+ data-required={dataAttr(isRequired)}
110
+ rightIcon={rightIcon}
111
+ ref={ref}
112
+ sx={buttonStyles}
113
+ >
114
+ {renderValue(displayValue) || placeholder}
115
+ </MenuButton>
116
+ )
80
117
  }
81
-
82
- // Using a Button, so we can simply use leftIcon and rightIcon
83
- return <MenuButton as={Button} {...props} ref={ref} sx={buttonStyles} />
84
- })
118
+ )
85
119
 
86
120
  SelectButton.displayName = 'SelectButton'
87
121
 
122
+ /**
123
+ * Allow users to select a value from a list of options.
124
+ *
125
+ * @see https://saas-ui.dev/docs/components/forms/select
126
+ */
88
127
  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
128
+ const { name, children, isDisabled, multiple, ...rest } = props
129
+
107
130
  const menuProps = omitThemingProps(rest)
108
131
 
109
- const [currentValue, setCurrentValue] = React.useState(value || defaultValue)
132
+ const context = useSelect(props)
110
133
 
111
- const controlProps = useFormControl({ name } as HTMLChakraProps<'input'>)
134
+ const { value, controlProps } = context
112
135
 
113
- const options = React.useMemo(
114
- () => optionsProp && mapOptions(optionsProp),
115
- [optionsProp]
136
+ return (
137
+ <SelectProvider value={context}>
138
+ <Menu {...menuProps} closeOnSelect={!multiple}>
139
+ <chakra.div className={cx('sui-select')}>
140
+ {children}
141
+ <chakra.input
142
+ {...controlProps}
143
+ ref={ref}
144
+ name={name}
145
+ type="hidden"
146
+ value={value || ''}
147
+ className="saas-select__input"
148
+ />
149
+ </chakra.div>
150
+ </Menu>
151
+ </SelectProvider>
116
152
  )
153
+ })
117
154
 
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
- )
155
+ export interface SelectListProps extends MenuListProps {}
147
156
 
148
- const displayValue = currentValue
149
- ? (Array.isArray(currentValue) ? currentValue : [currentValue]).map(
150
- getDisplayValue
151
- )
152
- : []
157
+ /**
158
+ * The list of options to choose from.
159
+ *
160
+ * @see https://saas-ui.dev/docs/components/forms/select
161
+ */
162
+ export const SelectList: React.FC<SelectListProps> = (props) => {
163
+ const { defaultValue, value, options, multiple, onChange } =
164
+ useSelectContext()
153
165
 
154
166
  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>
167
+ <MenuList maxH="100vh" overflowY="auto" {...props}>
168
+ <MenuOptionGroup
169
+ defaultValue={(defaultValue || value) as string | string[] | undefined}
170
+ value={value}
171
+ onChange={onChange}
172
+ type={multiple ? 'checkbox' : 'radio'}
173
+ >
174
+ {options
175
+ ? options.map(({ value, label, ...rest }, i) => (
176
+ <SelectOption key={i} value={value} {...rest}>
177
+ {label || value}
178
+ </SelectOption>
179
+ ))
180
+ : props.children}
181
+ </MenuOptionGroup>
182
+ </MenuList>
186
183
  )
187
- })
184
+ }
188
185
 
189
186
  Select.displayName = 'Select'
187
+
188
+ /**
189
+ * An option in a select list
190
+ *
191
+ * @see https://saas-ui.dev/docs/components/forms/select
192
+ */
193
+ export const SelectOption = forwardRef<MenuItemOptionProps, 'button'>(
194
+ (props, ref) => {
195
+ return <MenuItemOption ref={ref} {...props} />
196
+ }
197
+ )
198
+ SelectOption.id = 'MenuItemOption'
199
+ 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,