@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.
- package/CHANGELOG.md +20 -0
- package/dist/ajv/index.d.ts +358 -11
- package/dist/ajv/index.js +7 -9
- package/dist/ajv/index.js.map +1 -1
- package/dist/ajv/index.mjs +7 -10
- package/dist/ajv/index.mjs.map +1 -1
- package/dist/index.d.ts +296 -194
- package/dist/index.js +345 -2584
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +347 -2580
- package/dist/index.mjs.map +1 -1
- package/dist/yup/index.d.ts +573 -106
- package/dist/yup/index.js +6 -10
- package/dist/yup/index.js.map +1 -1
- package/dist/yup/index.mjs +4 -8
- package/dist/yup/index.mjs.map +1 -1
- package/dist/zod/index.d.ts +490 -14
- package/dist/zod/index.js +5 -0
- package/dist/zod/index.js.map +1 -1
- package/dist/zod/index.mjs +5 -1
- package/dist/zod/index.mjs.map +1 -1
- package/package.json +16 -9
- package/src/array-field.tsx +34 -17
- package/src/base-field.tsx +4 -9
- package/src/create-field.tsx +2 -1
- package/src/create-form.tsx +33 -10
- package/src/default-fields.tsx +21 -4
- package/src/display-field.tsx +1 -2
- package/src/display-if.tsx +14 -8
- package/src/field-resolver.ts +10 -8
- package/src/field.tsx +6 -3
- package/src/fields.tsx +16 -13
- package/src/form-context.tsx +84 -0
- package/src/form.tsx +44 -17
- package/src/index.ts +17 -15
- package/src/object-field.tsx +6 -2
- package/src/password-input/password-input.tsx +1 -1
- package/src/select/select-context.tsx +130 -0
- package/src/select/select.stories.tsx +116 -85
- package/src/select/select.tsx +154 -142
- package/src/types.ts +59 -6
- package/src/use-array-field.tsx +9 -3
- package/src/utils.ts +8 -1
- package/src/watch-field.tsx +2 -6
package/src/select/select.tsx
CHANGED
@@ -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 {
|
27
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
134
|
+
const context = useSelect(props)
|
110
135
|
|
111
|
-
const
|
136
|
+
const { value, controlProps } = context
|
112
137
|
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
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
|
-
<
|
156
|
-
<
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
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
|
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
|
package/src/use-array-field.tsx
CHANGED
@@ -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:
|
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}.${
|
16
|
+
name: `${name}.${childName}`,
|
10
17
|
})
|
11
18
|
}
|
12
19
|
return child
|
package/src/watch-field.tsx
CHANGED
@@ -1,9 +1,5 @@
|
|
1
|
-
import {
|
2
|
-
|
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,
|