@graphcommerce/ecommerce-ui 8.1.0-canary.9 → 9.0.0-canary.101

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.
@@ -1,11 +1,6 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import { IconSvg, iconClose } from '@graphcommerce/next-ui'
3
- import {
4
- Controller,
5
- FieldError,
6
- FieldValues,
7
- ControllerProps,
8
- } from '@graphcommerce/react-hook-form'
3
+ import { FieldValues, ControllerProps, useController } from '@graphcommerce/react-hook-form'
9
4
  import { i18n } from '@lingui/core'
10
5
  import {
11
6
  Checkbox,
@@ -27,8 +22,6 @@ export type MultiSelectElementProps<T extends FieldValues> = Omit<SelectProps, '
27
22
  itemValue?: string
28
23
  itemLabel?: string
29
24
  required?: boolean
30
- /** @deprecated Form value parsing should happen in the handleSubmit function of the form */
31
- parseError?: (error: FieldError) => string
32
25
  minWidth?: number
33
26
  menuMaxHeight?: number
34
27
  menuMaxWidth?: number
@@ -44,7 +37,7 @@ const ITEM_PADDING_TOP = 8
44
37
  export function MultiSelectElement<TFieldValues extends FieldValues>(
45
38
  props: MultiSelectElementProps<TFieldValues>,
46
39
  ): JSX.Element {
47
- let {
40
+ const {
48
41
  options,
49
42
  label = '',
50
43
  itemKey = 'id',
@@ -52,7 +45,6 @@ export function MultiSelectElement<TFieldValues extends FieldValues>(
52
45
  itemLabel = 'label',
53
46
  required = false,
54
47
  rules = {},
55
- parseError,
56
48
  name,
57
49
  menuMaxHeight = ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
58
50
  menuMaxWidth = 250,
@@ -62,114 +54,115 @@ export function MultiSelectElement<TFieldValues extends FieldValues>(
62
54
  control,
63
55
  showCheckbox,
64
56
  formControlProps,
57
+ shouldUnregister,
58
+ defaultValue,
59
+ disabled,
65
60
  ...rest
66
61
  } = props
67
62
  if (required && !rules.required) {
68
63
  rules.required = i18n._(/* i18n */ 'This field is required')
69
64
  }
70
65
 
66
+ const {
67
+ field: { value, onChange, ...field },
68
+ fieldState: { invalid, error },
69
+ } = useController({
70
+ name,
71
+ rules,
72
+ control,
73
+ defaultValue,
74
+ disabled,
75
+ shouldUnregister,
76
+ })
77
+
78
+ const parsedHelperText = error ? error.message : helperText
79
+
71
80
  return (
72
- <Controller
73
- name={name}
74
- rules={rules}
75
- control={control}
76
- render={({ field: { value, onChange, ...field }, fieldState: { invalid, error } }) => {
77
- helperText = error
78
- ? typeof parseError === 'function'
79
- ? parseError(error)
80
- : error.message
81
- : helperText
82
- return (
83
- <FormControl
84
- {...formControlProps}
85
- style={{
86
- ...formControlProps?.style,
87
- minWidth,
88
- }}
89
- variant={rest.variant}
90
- fullWidth={rest.fullWidth}
91
- error={invalid}
92
- size={rest.size}
93
- >
94
- {label && (
95
- <InputLabel
96
- size={rest.size === 'small' ? 'small' : undefined}
97
- error={invalid}
98
- htmlFor={rest.id || `select-multi-select-${name}`}
99
- required={required}
100
- >
101
- {label}
102
- </InputLabel>
103
- )}
104
- <Select
105
- {...rest}
106
- {...field}
107
- id={rest.id || `select-multi-select-${name}`}
108
- multiple
109
- label={label || undefined}
110
- error={invalid}
111
- value={value || []}
112
- required={required}
113
- onChange={onChange}
114
- MenuProps={{
115
- ...rest.MenuProps,
116
- PaperProps: {
117
- ...(rest.MenuProps?.PaperProps ?? {
118
- style: {
119
- maxHeight: menuMaxHeight,
120
- width: menuMaxWidth,
121
- ...rest.MenuProps?.PaperProps?.style,
122
- },
123
- }),
124
- },
81
+ <FormControl
82
+ {...formControlProps}
83
+ style={{
84
+ ...formControlProps?.style,
85
+ minWidth,
86
+ }}
87
+ variant={rest.variant}
88
+ fullWidth={rest.fullWidth}
89
+ error={invalid}
90
+ size={rest.size}
91
+ >
92
+ {label && (
93
+ <InputLabel
94
+ size={rest.size === 'small' ? 'small' : undefined}
95
+ error={invalid}
96
+ htmlFor={rest.id || `select-multi-select-${name}`}
97
+ required={required}
98
+ >
99
+ {label}
100
+ </InputLabel>
101
+ )}
102
+ <Select
103
+ {...rest}
104
+ {...field}
105
+ id={rest.id || `select-multi-select-${name}`}
106
+ multiple
107
+ label={label || undefined}
108
+ error={invalid}
109
+ value={value || []}
110
+ required={required}
111
+ onChange={onChange}
112
+ MenuProps={{
113
+ ...rest.MenuProps,
114
+ PaperProps: {
115
+ ...(rest.MenuProps?.PaperProps ?? {
116
+ style: {
117
+ maxHeight: menuMaxHeight,
118
+ width: menuMaxWidth,
119
+ ...rest.MenuProps?.PaperProps?.style,
120
+ },
121
+ }),
122
+ },
123
+ }}
124
+ renderValue={
125
+ typeof rest.renderValue === 'function'
126
+ ? rest.renderValue
127
+ : showChips
128
+ ? (selected) => (
129
+ <div style={{ display: 'flex', flexWrap: 'wrap' }}>
130
+ {((selected as any[]) || []).map((selectedValue) => (
131
+ <Chip
132
+ key={selectedValue}
133
+ label={selectedValue}
134
+ style={{ display: 'flex', flexWrap: 'wrap' }}
135
+ onDelete={() => {
136
+ onChange(value.filter((i: any) => i !== selectedValue))
137
+ // setValue(name, formValue.filter((i: any) => i !== value), { shouldValidate: true })
138
+ }}
139
+ deleteIcon={<IconSvg src={iconClose} />}
140
+ />
141
+ ))}
142
+ </div>
143
+ )
144
+ : (selected) => (Array.isArray(selected) ? selected.join(', ') : '')
145
+ }
146
+ >
147
+ {options.map((item) => {
148
+ const val: string | number = item[itemValue || itemKey] || item
149
+ const isChecked = Array.isArray(value) ? value.includes(val) : false
150
+ return (
151
+ <MenuItem
152
+ key={val}
153
+ value={val}
154
+ sx={{
155
+ fontWeight: (theme) =>
156
+ isChecked ? theme.typography.fontWeightBold : theme.typography.fontWeightRegular,
125
157
  }}
126
- renderValue={
127
- typeof rest.renderValue === 'function'
128
- ? rest.renderValue
129
- : showChips
130
- ? (selected) => (
131
- <div style={{ display: 'flex', flexWrap: 'wrap' }}>
132
- {((selected as any[]) || []).map((selectedValue) => (
133
- <Chip
134
- key={selectedValue}
135
- label={selectedValue}
136
- style={{ display: 'flex', flexWrap: 'wrap' }}
137
- onDelete={() => {
138
- onChange(value.filter((i: any) => i !== selectedValue))
139
- // setValue(name, formValue.filter((i: any) => i !== value), { shouldValidate: true })
140
- }}
141
- deleteIcon={<IconSvg src={iconClose} />}
142
- />
143
- ))}
144
- </div>
145
- )
146
- : (selected) => (Array.isArray(selected) ? selected.join(', ') : '')
147
- }
148
158
  >
149
- {options.map((item) => {
150
- const val: string | number = item[itemValue || itemKey] || item
151
- const isChecked = Array.isArray(value) ? value.includes(val) : false
152
- return (
153
- <MenuItem
154
- key={val}
155
- value={val}
156
- sx={{
157
- fontWeight: (theme) =>
158
- isChecked
159
- ? theme.typography.fontWeightBold
160
- : theme.typography.fontWeightRegular,
161
- }}
162
- >
163
- {showCheckbox && <Checkbox checked={isChecked} />}
164
- <ListItemText primary={item[itemLabel] || item} />
165
- </MenuItem>
166
- )
167
- })}
168
- </Select>
169
- {helperText && <FormHelperText>{helperText}</FormHelperText>}
170
- </FormControl>
171
- )
172
- }}
173
- />
159
+ {showCheckbox && <Checkbox checked={isChecked} />}
160
+ <ListItemText primary={item[itemLabel] || item} />
161
+ </MenuItem>
162
+ )
163
+ })}
164
+ </Select>
165
+ {parsedHelperText && <FormHelperText>{parsedHelperText}</FormHelperText>}
166
+ </FormControl>
174
167
  )
175
168
  }
@@ -44,6 +44,8 @@ export function NumberFieldElement<T extends FieldValues>(props: NumberFieldElem
44
44
  required,
45
45
  defaultValue,
46
46
  variant = 'outlined',
47
+ disabled,
48
+ shouldUnregister,
47
49
  ...textFieldProps
48
50
  } = props
49
51
 
@@ -61,6 +63,8 @@ export function NumberFieldElement<T extends FieldValues>(props: NumberFieldElem
61
63
  control,
62
64
  rules,
63
65
  defaultValue,
66
+ disabled,
67
+ shouldUnregister,
64
68
  })
65
69
 
66
70
  const valueAsNumber = value ? parseFloat(value) : 0
@@ -115,7 +119,11 @@ export function NumberFieldElement<T extends FieldValues>(props: NumberFieldElem
115
119
  aria-label={i18n._(/* i18n */ 'Decrease')}
116
120
  size='smaller'
117
121
  onClick={() => {
118
- if ((valueAsNumber || Infinity) <= inputProps.min) return
122
+ if (
123
+ (valueAsNumber ?? Infinity) <= inputProps.min ||
124
+ (inputProps.min === 0 && valueAsNumber <= inputProps.min)
125
+ )
126
+ return
119
127
  onChange(value - 1)
120
128
  }}
121
129
  sx={{
@@ -1,9 +1,4 @@
1
- import {
2
- FieldError,
3
- useController,
4
- FieldValues,
5
- UseControllerProps,
6
- } from '@graphcommerce/react-hook-form'
1
+ import { useController, FieldValues, UseControllerProps } from '@graphcommerce/react-hook-form'
7
2
  import { i18n } from '@lingui/core'
8
3
  import {
9
4
  FormControl,
@@ -20,8 +15,6 @@ export type RadioButtonGroupProps<T extends FieldValues> = {
20
15
  options: { label: string; id: string | number }[] | any[]
21
16
  helperText?: string
22
17
  required?: boolean
23
- /** @deprecated Form value parsing should happen in the handleSubmit function of the form */
24
- parseError?: (error: FieldError) => string
25
18
  label?: string
26
19
  labelKey?: string
27
20
  valueKey?: string
@@ -32,21 +25,27 @@ export type RadioButtonGroupProps<T extends FieldValues> = {
32
25
  row?: boolean
33
26
  } & UseControllerProps<T>
34
27
 
35
- export function RadioButtonGroup<TFieldValues extends FieldValues>({
36
- helperText,
37
- options,
38
- label,
39
- name,
40
- parseError,
41
- labelKey = 'label',
42
- valueKey = 'id',
43
- required,
44
- emptyOptionLabel,
45
- returnObject,
46
- row,
47
- control,
48
- ...rest
49
- }: RadioButtonGroupProps<TFieldValues>): JSX.Element {
28
+ export function RadioButtonGroup<TFieldValues extends FieldValues>(
29
+ props: RadioButtonGroupProps<TFieldValues>,
30
+ ): JSX.Element {
31
+ const {
32
+ helperText,
33
+ options,
34
+ label,
35
+ name,
36
+ labelKey = 'label',
37
+ valueKey = 'id',
38
+ required,
39
+ emptyOptionLabel,
40
+ returnObject,
41
+ row,
42
+ control,
43
+ defaultValue,
44
+ disabled,
45
+ shouldUnregister,
46
+ ...rest
47
+ } = props
48
+
50
49
  const theme = useTheme()
51
50
  const {
52
51
  field: { value, onChange },
@@ -55,13 +54,12 @@ export function RadioButtonGroup<TFieldValues extends FieldValues>({
55
54
  name,
56
55
  rules: required ? { required: i18n._(/* i18n */ 'This field is required') } : undefined,
57
56
  control,
57
+ defaultValue,
58
+ disabled,
59
+ shouldUnregister,
58
60
  })
59
61
 
60
- helperText = error
61
- ? typeof parseError === 'function'
62
- ? parseError(error)
63
- : error.message
64
- : helperText
62
+ const parsedHelperText = error ? error.message : helperText
65
63
 
66
64
  const onRadioChange = (event: ChangeEvent<HTMLInputElement>) => {
67
65
  const radioValue = (event.target as HTMLInputElement).value
@@ -125,7 +123,7 @@ export function RadioButtonGroup<TFieldValues extends FieldValues>({
125
123
  )
126
124
  })}
127
125
  </RadioGroup>
128
- {helperText && <FormHelperText>{helperText}</FormHelperText>}
126
+ {parsedHelperText && <FormHelperText>{parsedHelperText}</FormHelperText>}
129
127
  </FormControl>
130
128
  )
131
129
  }
@@ -1,8 +1,8 @@
1
1
  import {
2
- Controller,
3
2
  ControllerProps,
4
3
  FieldError,
5
4
  FieldValues,
5
+ useController,
6
6
  } from '@graphcommerce/react-hook-form'
7
7
  import { i18n } from '@lingui/core'
8
8
  import {
@@ -16,8 +16,6 @@ import {
16
16
 
17
17
  export type SliderElementProps<T extends FieldValues> = Omit<SliderProps, 'control'> & {
18
18
  label?: string
19
- /** @deprecated Form value parsing should happen in the handleSubmit function of the form */
20
- parseError?: (error: FieldError) => string
21
19
  required?: boolean
22
20
  formControlProps?: FormControlProps
23
21
  } & Omit<ControllerProps<T>, 'render'>
@@ -27,39 +25,40 @@ export function SliderElement<TFieldValues extends FieldValues>({
27
25
  control,
28
26
  label,
29
27
  rules = {},
30
- parseError,
31
28
  required,
32
29
  formControlProps,
30
+ defaultValue,
31
+ disabled,
32
+ shouldUnregister,
33
33
  ...other
34
34
  }: SliderElementProps<TFieldValues>) {
35
35
  if (required && !rules.required) {
36
36
  rules.required = i18n._(/* i18n */ 'This field is required')
37
37
  }
38
+
39
+ const {
40
+ field,
41
+ fieldState: { invalid, error },
42
+ } = useController({
43
+ name,
44
+ control,
45
+ rules,
46
+ defaultValue,
47
+ disabled,
48
+ shouldUnregister,
49
+ })
50
+
51
+ const parsedHelperText = error ? error.message : null
52
+
38
53
  return (
39
- <Controller
40
- name={name}
41
- control={control}
42
- rules={rules}
43
- render={({ field, fieldState: { invalid, error } }) => {
44
- const parsedHelperText = error
45
- ? typeof parseError === 'function'
46
- ? parseError(error)
47
- : error.message
48
- : null
49
- return (
50
- <FormControl error={invalid} required={required} fullWidth {...formControlProps}>
51
- {label && (
52
- <FormLabel component='legend' error={invalid}>
53
- {label}
54
- </FormLabel>
55
- )}
56
- <Slider {...other} {...field} valueLabelDisplay={other.valueLabelDisplay || 'auto'} />
57
- {parsedHelperText && (
58
- <FormHelperText error={invalid}>{parsedHelperText}</FormHelperText>
59
- )}
60
- </FormControl>
61
- )
62
- }}
63
- />
54
+ <FormControl error={invalid} required={required} fullWidth {...formControlProps}>
55
+ {label && (
56
+ <FormLabel component='legend' error={invalid}>
57
+ {label}
58
+ </FormLabel>
59
+ )}
60
+ <Slider {...other} {...field} valueLabelDisplay={other.valueLabelDisplay || 'auto'} />
61
+ {parsedHelperText && <FormHelperText error={invalid}>{parsedHelperText}</FormHelperText>}
62
+ </FormControl>
64
63
  )
65
64
  }
@@ -1,4 +1,4 @@
1
- import { Controller, FieldValues, ControllerProps } from '@graphcommerce/react-hook-form'
1
+ import { FieldValues, ControllerProps, useController } from '@graphcommerce/react-hook-form'
2
2
  import { FormControlLabel, FormControlLabelProps, Switch } from '@mui/material'
3
3
 
4
4
  type IProps = Omit<FormControlLabelProps, 'control'>
@@ -8,18 +8,20 @@ export type SwitchElementProps<T extends FieldValues> = IProps & Omit<Controller
8
8
  export function SwitchElement<TFieldValues extends FieldValues>({
9
9
  name,
10
10
  control,
11
+ defaultValue,
12
+ disabled,
13
+ shouldUnregister,
14
+ rules,
11
15
  ...other
12
16
  }: SwitchElementProps<TFieldValues>) {
13
- return (
14
- <FormControlLabel
15
- control={
16
- <Controller
17
- name={name}
18
- control={control}
19
- render={({ field }) => <Switch {...field} checked={!!field.value} />}
20
- />
21
- }
22
- {...other}
23
- />
24
- )
17
+ const { field } = useController({
18
+ name,
19
+ control,
20
+ defaultValue,
21
+ disabled,
22
+ shouldUnregister,
23
+ rules,
24
+ })
25
+
26
+ return <FormControlLabel control={<Switch {...field} checked={!!field.value} />} {...other} />
25
27
  }
@@ -0,0 +1,23 @@
1
+ import { FieldValues, phonePattern } from '@graphcommerce/react-hook-form'
2
+ import { Trans, t } from '@lingui/macro'
3
+ import { TextFieldElement, TextFieldElementProps } from './TextFieldElement'
4
+
5
+ export type TelephoneElementProps<T extends FieldValues> = TextFieldElementProps<T>
6
+
7
+ export function TelephoneElement<TFieldValues extends FieldValues>(
8
+ props: TelephoneElementProps<TFieldValues>,
9
+ ): JSX.Element {
10
+ const { rules, ...rest } = props
11
+ return (
12
+ <TextFieldElement
13
+ type='text'
14
+ label={<Trans>Telephone</Trans>}
15
+ autoComplete='tel'
16
+ rules={{
17
+ pattern: { value: phonePattern, message: t`Invalid phone number` },
18
+ ...rules,
19
+ }}
20
+ {...rest}
21
+ />
22
+ )
23
+ }
@@ -1,8 +1,14 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import { InputCheckmark } from '@graphcommerce/next-ui'
3
- import { FieldValues, UseControllerProps, useController } from '@graphcommerce/react-hook-form'
3
+ import {
4
+ FieldValues,
5
+ UseControllerProps,
6
+ emailPattern,
7
+ useController,
8
+ } from '@graphcommerce/react-hook-form'
4
9
  import { i18n } from '@lingui/core'
5
10
  import { TextField, TextFieldProps } from '@mui/material'
11
+ import React, { useState } from 'react'
6
12
 
7
13
  export type TextFieldElementProps<T extends FieldValues = FieldValues> = Omit<
8
14
  TextFieldProps,
@@ -22,7 +28,9 @@ export function TextFieldElement<TFieldValues extends FieldValues>({
22
28
  control,
23
29
  defaultValue,
24
30
  rules = validation,
31
+ shouldUnregister,
25
32
  showValid,
33
+ disabled,
26
34
  ...rest
27
35
  }: TextFieldElementProps<TFieldValues>): JSX.Element {
28
36
  if (required && !rules.required) {
@@ -31,9 +39,7 @@ export function TextFieldElement<TFieldValues extends FieldValues>({
31
39
 
32
40
  if (type === 'email' && !rules.pattern) {
33
41
  rules.pattern = {
34
- // eslint-disable-next-line no-useless-escape
35
- value:
36
- /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
42
+ value: emailPattern,
37
43
  message: i18n._(/* i18n */ 'Please enter a valid email address'),
38
44
  }
39
45
  }
@@ -41,13 +47,26 @@ export function TextFieldElement<TFieldValues extends FieldValues>({
41
47
  const {
42
48
  field: { onChange, ref, value = '', ...field },
43
49
  fieldState: { error },
44
- } = useController({ name, control, rules, defaultValue })
50
+ } = useController({ name, control, rules, defaultValue, shouldUnregister, disabled })
51
+
52
+ // https://stackoverflow.com/questions/76830737/chrome-autofill-causes-textbox-collision-for-textfield-label-and-value
53
+ const [hasAutofill, setHasAutofill] = useState(false)
54
+ const shrink = hasAutofill || rest.InputLabelProps?.shrink || Boolean(value)
55
+ const onAnimationStart = (e: React.AnimationEvent<HTMLInputElement | HTMLTextAreaElement>) => {
56
+ if (e.target instanceof HTMLElement) {
57
+ const autofilled = !!e.target.matches('*:-webkit-autofill')
58
+ if (e.animationName === 'mui-auto-fill') setHasAutofill(autofilled)
59
+ if (e.animationName === 'mui-auto-fill-cancel') setHasAutofill(autofilled)
60
+ }
61
+ return rest.inputProps?.onAnimationStart?.(e)
62
+ }
45
63
 
46
64
  return (
47
65
  <TextField
48
66
  {...rest}
49
67
  {...field}
50
68
  value={value}
69
+ inputProps={{ ...rest.inputProps, onAnimationStart }}
51
70
  onChange={(ev) => {
52
71
  onChange(type === 'number' && ev.target.value ? Number(ev.target.value) : ev.target.value)
53
72
  rest.onChange?.(ev)
@@ -57,6 +76,7 @@ export function TextFieldElement<TFieldValues extends FieldValues>({
57
76
  type={type}
58
77
  error={Boolean(error) || rest.error}
59
78
  helperText={error ? error.message : rest.helperText}
79
+ InputLabelProps={{ ...rest.InputLabelProps, shrink }}
60
80
  InputProps={{
61
81
  ...rest.InputProps,
62
82
  endAdornment: