@graphcommerce/ecommerce-ui 8.0.6-canary.1 → 8.0.6-canary.2

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @graphcommerce/ecommerce-ui
2
2
 
3
+ ## 8.0.6-canary.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2234](https://github.com/graphcommerce-org/graphcommerce/pull/2234) [`0767bc4`](https://github.com/graphcommerce-org/graphcommerce/commit/0767bc40f7b596209f24ca4e745ff0441f3275c9) - Upgrade input components to no longer use muiRegister, which improves INP scores
8
+ ([@FrankHarland](https://github.com/FrankHarland))
9
+
10
+ - [#2234](https://github.com/graphcommerce-org/graphcommerce/pull/2234) [`43bd04a`](https://github.com/graphcommerce-org/graphcommerce/commit/43bd04a777c5800cc7e01bee1e123a5aad82f194) - Make sure the TextFieldElement doesn’t give a uncontrolled to controlled warning.
11
+ Convert SelectElement to useController instead of a separate Controller component.
12
+ Make sure the original endAdornment is always shown only until the value is valid ([@FrankHarland](https://github.com/FrankHarland))
13
+
14
+ - [#2234](https://github.com/graphcommerce-org/graphcommerce/pull/2234) [`d4e693d`](https://github.com/graphcommerce-org/graphcommerce/commit/d4e693d553198c9a1ef398d000ca23d209e6c2ba) - The `<WaitForQueries/>` component now uses the useIsSSR hook which prevents loading spinners when navigating on the client, which make all account/cart/checkout pages faster.
15
+ ([@FrankHarland](https://github.com/FrankHarland))
16
+
3
17
  ## 8.0.6-canary.1
4
18
 
5
19
  ## 8.0.6-canary.0
@@ -5,7 +5,12 @@ import {
5
5
  IconSvg,
6
6
  responsiveVal,
7
7
  } from '@graphcommerce/next-ui'
8
- import { Controller, ControllerProps, FieldValues } from '@graphcommerce/react-hook-form'
8
+ import {
9
+ Controller,
10
+ ControllerProps,
11
+ FieldValues,
12
+ useController,
13
+ } from '@graphcommerce/react-hook-form'
9
14
  import { i18n } from '@lingui/core'
10
15
  import { IconButtonProps, SxProps, Theme, TextField, TextFieldProps, Fab } from '@mui/material'
11
16
 
@@ -48,117 +53,117 @@ export function NumberFieldElement<T extends FieldValues>(props: NumberFieldElem
48
53
  rules.required = i18n._(/* i18n */ 'This field is required')
49
54
  }
50
55
 
51
- return (
52
- <Controller
53
- name={name}
54
- control={control}
55
- rules={rules}
56
- defaultValue={defaultValue}
57
- render={({ field: { value, onChange, ref, ...field }, fieldState: { invalid, error } }) => {
58
- const valueAsNumber = value ? parseFloat(value) : 0
56
+ const {
57
+ field: { value, onChange, ref, ...field },
58
+ fieldState: { invalid, error },
59
+ } = useController({
60
+ name,
61
+ control,
62
+ rules,
63
+ defaultValue,
64
+ })
59
65
 
60
- return (
61
- <TextField
62
- {...textFieldProps}
63
- {...field}
64
- inputRef={ref}
65
- value={value ?? ''}
66
- onChange={(ev) => {
67
- const newValue = (ev.target as HTMLInputElement).valueAsNumber
68
- onChange(Number.isNaN(newValue) ? '' : newValue)
69
- textFieldProps.onChange?.(ev)
66
+ const valueAsNumber = value ? parseFloat(value) : 0
67
+
68
+ return (
69
+ <TextField
70
+ {...textFieldProps}
71
+ {...field}
72
+ inputRef={ref}
73
+ value={value ?? ''}
74
+ onChange={(ev) => {
75
+ const newValue = (ev.target as HTMLInputElement).valueAsNumber
76
+ onChange(Number.isNaN(newValue) ? '' : newValue)
77
+ textFieldProps.onChange?.(ev)
78
+ }}
79
+ variant={variant}
80
+ required={required}
81
+ error={invalid}
82
+ helperText={error ? error.message : textFieldProps.helperText}
83
+ size={size}
84
+ type='number'
85
+ className={`${textFieldProps.className ?? ''} ${classes.quantity}`}
86
+ sx={[
87
+ {
88
+ width: responsiveVal(90, 120),
89
+ },
90
+ {
91
+ '& input[type=number]': {
92
+ MozAppearance: 'textfield',
93
+ },
94
+ '& .MuiOutlinedInput-root': {
95
+ px: '2px',
96
+ display: 'grid',
97
+ gridTemplateColumns: '1fr auto 1fr',
98
+ },
99
+ },
100
+ variant === 'standard' && {
101
+ '& .MuiOutlinedInput-input': {
102
+ padding: 0,
103
+ },
104
+ '& .MuiOutlinedInput-notchedOutline': {
105
+ display: 'none',
106
+ },
107
+ },
108
+ ...(Array.isArray(sx) ? sx : [sx]),
109
+ ]}
110
+ autoComplete='off'
111
+ InputProps={{
112
+ ...textFieldProps.InputProps,
113
+ startAdornment: (
114
+ <Fab
115
+ aria-label={i18n._(/* i18n */ 'Decrease')}
116
+ size='smaller'
117
+ onClick={() => {
118
+ if ((valueAsNumber || Infinity) <= inputProps.min) return
119
+ onChange(value - 1)
70
120
  }}
71
- variant={variant}
72
- required={required}
73
- error={invalid}
74
- helperText={error ? error.message : textFieldProps.helperText}
75
- size={size}
76
- type='number'
77
- className={`${textFieldProps.className ?? ''} ${classes.quantity}`}
78
- sx={[
79
- {
80
- width: responsiveVal(90, 120),
81
- },
82
- {
83
- '& input[type=number]': {
84
- MozAppearance: 'textfield',
85
- },
86
- '& .MuiOutlinedInput-root': {
87
- px: '2px',
88
- display: 'grid',
89
- gridTemplateColumns: '1fr auto 1fr',
90
- },
91
- },
92
- variant === 'standard' && {
93
- '& .MuiOutlinedInput-input': {
94
- padding: 0,
95
- },
96
- '& .MuiOutlinedInput-notchedOutline': {
97
- display: 'none',
98
- },
99
- },
100
- ...(Array.isArray(sx) ? sx : [sx]),
101
- ]}
102
- autoComplete='off'
103
- InputProps={{
104
- ...textFieldProps.InputProps,
105
- startAdornment: (
106
- <Fab
107
- aria-label={i18n._(/* i18n */ 'Decrease')}
108
- size='smaller'
109
- onClick={() => {
110
- if ((valueAsNumber || Infinity) <= inputProps.min) return
111
- onChange(value - 1)
112
- }}
113
- sx={{
114
- boxShadow: variant === 'standard' ? 4 : 0,
115
- minHeight: '30px',
116
- minWidth: '30px',
117
- }}
118
- tabIndex={-1}
119
- color='inherit'
120
- {...DownProps}
121
- className={`${classes.button} ${DownProps.className ?? ''}`}
122
- >
123
- {DownProps.children ?? <IconSvg src={iconMin} size='small' />}
124
- </Fab>
125
- ),
126
- endAdornment: (
127
- <Fab
128
- aria-label={i18n._(/* i18n */ 'Increase')}
129
- size='smaller'
130
- onClick={() => {
131
- if (valueAsNumber >= (inputProps.max ?? Infinity)) return
132
- onChange(valueAsNumber + 1)
133
- }}
134
- sx={{
135
- boxShadow: variant === 'standard' ? 4 : 0,
136
- minHeight: '30px',
137
- minWidth: '30px',
138
- }}
139
- tabIndex={-1}
140
- color='inherit'
141
- {...UpProps}
142
- className={`${classes.button} ${UpProps.className ?? ''}`}
143
- >
144
- {UpProps.children ?? <IconSvg src={iconPlus} size='small' />}
145
- </Fab>
146
- ),
121
+ sx={{
122
+ boxShadow: variant === 'standard' ? 4 : 0,
123
+ minHeight: '30px',
124
+ minWidth: '30px',
147
125
  }}
148
- inputProps={{
149
- 'aria-label': i18n._(/* i18n */ 'Number'),
150
- ...inputProps,
151
- className: `${inputProps?.className ?? ''} ${classes.quantityInput}`,
152
- sx: {
153
- typography: 'body1',
154
- textAlign: 'center',
155
- '&::-webkit-inner-spin-button,&::-webkit-outer-spin-button': {
156
- appearance: 'none',
157
- },
158
- },
126
+ tabIndex={-1}
127
+ color='inherit'
128
+ {...DownProps}
129
+ className={`${classes.button} ${DownProps.className ?? ''}`}
130
+ >
131
+ {DownProps.children ?? <IconSvg src={iconMin} size='small' />}
132
+ </Fab>
133
+ ),
134
+ endAdornment: (
135
+ <Fab
136
+ aria-label={i18n._(/* i18n */ 'Increase')}
137
+ size='smaller'
138
+ onClick={() => {
139
+ if (valueAsNumber >= (inputProps.max ?? Infinity)) return
140
+ onChange(valueAsNumber + 1)
159
141
  }}
160
- />
161
- )
142
+ sx={{
143
+ boxShadow: variant === 'standard' ? 4 : 0,
144
+ minHeight: '30px',
145
+ minWidth: '30px',
146
+ }}
147
+ tabIndex={-1}
148
+ color='inherit'
149
+ {...UpProps}
150
+ className={`${classes.button} ${UpProps.className ?? ''}`}
151
+ >
152
+ {UpProps.children ?? <IconSvg src={iconPlus} size='small' />}
153
+ </Fab>
154
+ ),
155
+ }}
156
+ inputProps={{
157
+ 'aria-label': i18n._(/* i18n */ 'Number'),
158
+ ...inputProps,
159
+ className: `${inputProps?.className ?? ''} ${classes.quantityInput}`,
160
+ sx: {
161
+ typography: 'body1',
162
+ textAlign: 'center',
163
+ '&::-webkit-inner-spin-button,&::-webkit-outer-spin-button': {
164
+ appearance: 'none',
165
+ },
166
+ },
162
167
  }}
163
168
  />
164
169
  )
@@ -1,4 +1,10 @@
1
- import { Controller, ControllerProps, FieldValues } from '@graphcommerce/react-hook-form'
1
+ import { InputCheckmark } from '@graphcommerce/next-ui'
2
+ import {
3
+ Controller,
4
+ ControllerProps,
5
+ FieldValues,
6
+ useController,
7
+ } from '@graphcommerce/react-hook-form'
2
8
  import { i18n } from '@lingui/core'
3
9
  import { MenuItem, TextField, TextFieldProps } from '@mui/material'
4
10
 
@@ -13,6 +19,7 @@ export type SelectElementProps<T extends FieldValues, O extends OptionBase> = Om
13
19
  options?: O[]
14
20
  type?: 'string' | 'number'
15
21
  onChange?: (value: string | number) => void
22
+ showValid?: boolean
16
23
  } & Omit<ControllerProps<T>, 'render'>
17
24
 
18
25
  export function SelectElement<TFieldValues extends FieldValues, O extends OptionBase>({
@@ -24,6 +31,9 @@ export function SelectElement<TFieldValues extends FieldValues, O extends Option
24
31
  control,
25
32
  defaultValue,
26
33
  rules = validation ?? {},
34
+ showValid,
35
+ disabled,
36
+ shouldUnregister,
27
37
  ...rest
28
38
  }: SelectElementProps<TFieldValues, O>): JSX.Element {
29
39
  const isNativeSelect = !!rest.SelectProps?.native
@@ -33,45 +43,56 @@ export function SelectElement<TFieldValues extends FieldValues, O extends Option
33
43
  rules.required = i18n._(/* i18n */ 'This field is required')
34
44
  }
35
45
 
36
- return (
37
- <Controller
38
- name={name}
39
- rules={rules}
40
- control={control}
41
- defaultValue={defaultValue}
42
- render={({ field: { onChange, value, ref, ...field }, fieldState: { invalid, error } }) => {
43
- // handle shrink on number input fields
44
- if (type === 'number' && typeof value !== 'undefined') {
45
- rest.InputLabelProps = rest.InputLabelProps || {}
46
- rest.InputLabelProps.shrink = true
47
- }
46
+ const {
47
+ field: { onChange, value, ref, ...field },
48
+ fieldState: { invalid, error },
49
+ } = useController({
50
+ name,
51
+ rules,
52
+ control,
53
+ defaultValue,
54
+ disabled,
55
+ shouldUnregister,
56
+ })
57
+
58
+ // handle shrink on number input fields
59
+ if (type === 'number' && typeof value !== 'undefined') {
60
+ rest.InputLabelProps = rest.InputLabelProps || {}
61
+ rest.InputLabelProps.shrink = true
62
+ }
48
63
 
49
- return (
50
- <TextField
51
- {...rest}
52
- value={value ?? ''}
53
- {...field}
54
- inputRef={ref}
55
- onChange={(event) => {
56
- let item: number | string | O | undefined = event.target.value
57
- if (type === 'number') item = Number(item)
58
- rest.onChange?.(item)
59
- onChange(item)
60
- }}
61
- select
62
- required={required}
63
- error={invalid}
64
- helperText={error ? error.message : rest.helperText}
65
- >
66
- {isNativeSelect && <option />}
67
- {options.map((item) => (
68
- <ChildComponent key={item.id} value={item.id}>
69
- {item.label}
70
- </ChildComponent>
71
- ))}
72
- </TextField>
73
- )
64
+ return (
65
+ <TextField
66
+ {...rest}
67
+ value={value ?? ''}
68
+ {...field}
69
+ inputRef={ref}
70
+ onChange={(event) => {
71
+ let item: number | string | O | undefined = event.target.value
72
+ if (type === 'number') item = Number(item)
73
+ rest.onChange?.(item)
74
+ onChange(item)
75
+ }}
76
+ select
77
+ required={required}
78
+ error={invalid}
79
+ helperText={error ? error.message : rest.helperText}
80
+ InputProps={{
81
+ ...rest.InputProps,
82
+ endAdornment:
83
+ showValid && value && !error ? (
84
+ <InputCheckmark show={!error} />
85
+ ) : (
86
+ rest.InputProps?.endAdornment
87
+ ),
74
88
  }}
75
- />
89
+ >
90
+ {isNativeSelect && <option />}
91
+ {options.map((item) => (
92
+ <ChildComponent key={item.id} value={item.id}>
93
+ {item.label}
94
+ </ChildComponent>
95
+ ))}
96
+ </TextField>
76
97
  )
77
98
  }
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable no-nested-ternary */
2
- import { Controller, FieldValues, UseControllerProps } from '@graphcommerce/react-hook-form'
2
+ import { InputCheckmark } from '@graphcommerce/next-ui'
3
+ import { FieldValues, UseControllerProps, useController } from '@graphcommerce/react-hook-form'
3
4
  import { i18n } from '@lingui/core'
4
5
  import { TextField, TextFieldProps } from '@mui/material'
5
6
 
@@ -9,6 +10,8 @@ export type TextFieldElementProps<T extends FieldValues = FieldValues> = Omit<
9
10
  > & {
10
11
  /** @deprecated Please use the rules props instead */
11
12
  validation?: UseControllerProps<T>['rules']
13
+
14
+ showValid?: boolean
12
15
  } & UseControllerProps<T>
13
16
 
14
17
  export function TextFieldElement<TFieldValues extends FieldValues>({
@@ -19,6 +22,7 @@ export function TextFieldElement<TFieldValues extends FieldValues>({
19
22
  control,
20
23
  defaultValue,
21
24
  rules = validation,
25
+ showValid,
22
26
  ...rest
23
27
  }: TextFieldElementProps<TFieldValues>): JSX.Element {
24
28
  if (required && !rules.required) {
@@ -34,29 +38,34 @@ export function TextFieldElement<TFieldValues extends FieldValues>({
34
38
  }
35
39
  }
36
40
 
41
+ const {
42
+ field: { onChange, ref, value = '', ...field },
43
+ fieldState: { error },
44
+ } = useController({ name, control, rules, defaultValue })
45
+
37
46
  return (
38
- <Controller
39
- name={name}
40
- control={control}
41
- rules={rules}
42
- defaultValue={defaultValue}
43
- render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => (
44
- <TextField
45
- {...rest}
46
- {...field}
47
- onChange={(ev) => {
48
- onChange(
49
- type === 'number' && ev.target.value ? Number(ev.target.value) : ev.target.value,
50
- )
51
- rest.onChange?.(ev)
52
- }}
53
- inputRef={ref}
54
- required={required}
55
- type={type}
56
- error={Boolean(error) || rest.error}
57
- helperText={error ? error.message : rest.helperText}
58
- />
59
- )}
47
+ <TextField
48
+ {...rest}
49
+ {...field}
50
+ value={value}
51
+ onChange={(ev) => {
52
+ onChange(type === 'number' && ev.target.value ? Number(ev.target.value) : ev.target.value)
53
+ rest.onChange?.(ev)
54
+ }}
55
+ inputRef={ref}
56
+ required={required}
57
+ type={type}
58
+ error={Boolean(error) || rest.error}
59
+ helperText={error ? error.message : rest.helperText}
60
+ InputProps={{
61
+ ...rest.InputProps,
62
+ endAdornment:
63
+ showValid && value && !error ? (
64
+ <InputCheckmark show={!error} />
65
+ ) : (
66
+ rest.InputProps?.endAdornment
67
+ ),
68
+ }}
60
69
  />
61
70
  )
62
71
  }
@@ -1,9 +1,12 @@
1
- import { useIsomorphicLayoutEffect } from '@graphcommerce/framer-utils'
2
1
  import { QueryResult } from '@graphcommerce/graphql'
3
- import React, { startTransition, useEffect, useState } from 'react'
2
+ import { useIsSSR } from '@graphcommerce/next-ui'
3
+ import React from 'react'
4
4
 
5
5
  export type WaitForQueriesProps = {
6
6
  waitFor: QueryResult<any, any> | boolean | (QueryResult<any, any> | boolean)[] | undefined
7
+ /**
8
+ * @deprecated Will be automatically correct.
9
+ */
7
10
  noSsr?: boolean
8
11
  children: React.ReactNode
9
12
  fallback?: React.ReactNode
@@ -11,14 +14,9 @@ export type WaitForQueriesProps = {
11
14
 
12
15
  /** Shows the fallback during: SSR, Hydration and Query Loading. */
13
16
  export const WaitForQueries = (props: WaitForQueriesProps) => {
14
- const { waitFor, fallback, children, noSsr = true } = props
17
+ const { waitFor, fallback, children } = props
15
18
 
16
- // Make sure the first render is always the same as the server.
17
- // Make sure we we use startTransition to make sure we don't get into trouble with Suspense.
18
- const [mounted, setMounted] = useState(!noSsr)
19
- useEffect(() => {
20
- if (noSsr) setMounted(true)
21
- }, [noSsr])
19
+ const mounted = !useIsSSR()
22
20
 
23
21
  // We are done when all queries either have data or an error.
24
22
  const isDone = (Array.isArray(waitFor) ? waitFor : [waitFor]).every((res) => {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/ecommerce-ui",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "8.0.6-canary.1",
5
+ "version": "8.0.6-canary.2",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -12,12 +12,12 @@
12
12
  }
13
13
  },
14
14
  "peerDependencies": {
15
- "@graphcommerce/eslint-config-pwa": "^8.0.6-canary.1",
16
- "@graphcommerce/graphql": "^8.0.6-canary.1",
17
- "@graphcommerce/next-ui": "^8.0.6-canary.1",
18
- "@graphcommerce/prettier-config-pwa": "^8.0.6-canary.1",
19
- "@graphcommerce/react-hook-form": "^8.0.6-canary.1",
20
- "@graphcommerce/typescript-config-pwa": "^8.0.6-canary.1",
15
+ "@graphcommerce/eslint-config-pwa": "^8.0.6-canary.2",
16
+ "@graphcommerce/graphql": "^8.0.6-canary.2",
17
+ "@graphcommerce/next-ui": "^8.0.6-canary.2",
18
+ "@graphcommerce/prettier-config-pwa": "^8.0.6-canary.2",
19
+ "@graphcommerce/react-hook-form": "^8.0.6-canary.2",
20
+ "@graphcommerce/typescript-config-pwa": "^8.0.6-canary.2",
21
21
  "@lingui/core": "^4.2.1",
22
22
  "@lingui/macro": "^4.2.1",
23
23
  "@lingui/react": "^4.2.1",