@graphcommerce/next-ui 4.26.0 → 4.28.0

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,9 +1,25 @@
1
- import { SxProps, ButtonBase, Box, Theme, alpha } from '@mui/material'
1
+ import { alpha, Box, BoxProps, ButtonBase, ButtonProps, SxProps, Theme } from '@mui/material'
2
2
  import React from 'react'
3
3
  import { extendableComponent } from '../Styles'
4
4
  import { breakpointVal } from '../Styles/breakpointVal'
5
5
 
6
+ type Variants = 'outlined' | 'default'
7
+ type Size = 'large' | 'medium' | 'small'
8
+ type Color = 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning'
9
+ type Layout = 'inline' | 'grid' | 'list' | 'stack'
10
+
11
+ function isButtonProps(props: ButtonProps<'div'> | BoxProps<'div'>): props is ButtonProps<'div'> {
12
+ return props.onClick !== undefined
13
+ }
14
+
15
+ const RenderComponent = (props: ButtonProps<'div'> | BoxProps<'div'>) =>
16
+ isButtonProps(props) ? <ButtonBase component='div' {...props} /> : <Box {...props} />
17
+
6
18
  export type ActionCardProps = {
19
+ variant?: Variants
20
+ size?: Size
21
+ color?: Color
22
+ layout?: Layout
7
23
  sx?: SxProps<Theme>
8
24
  title?: string | React.ReactNode
9
25
  image?: React.ReactNode
@@ -14,10 +30,10 @@ export type ActionCardProps = {
14
30
  secondaryAction?: React.ReactNode
15
31
  onClick?: (event: React.MouseEvent<HTMLElement>, value: string | number) => void
16
32
  selected?: boolean
17
- hidden?: boolean
18
33
  value: string | number
19
34
  reset?: React.ReactNode
20
35
  disabled?: boolean
36
+ error?: boolean
21
37
  }
22
38
 
23
39
  const parts = [
@@ -34,10 +50,14 @@ const parts = [
34
50
  const name = 'ActionCard'
35
51
 
36
52
  type StateProps = {
37
- selected?: boolean
38
- hidden?: boolean
39
- disabled?: boolean
40
- image?: boolean
53
+ variant: Variants
54
+ size: Size
55
+ color: Color
56
+ layout: Layout
57
+ selected: boolean
58
+ disabled: boolean
59
+ image: boolean
60
+ error: boolean
41
61
  }
42
62
 
43
63
  const { withState, selectors } = extendableComponent<StateProps, typeof name, typeof parts>(
@@ -60,99 +80,131 @@ export function ActionCard(props: ActionCardProps) {
60
80
  onClick,
61
81
  value,
62
82
  selected = false,
63
- hidden = false,
64
83
  reset,
65
84
  disabled = false,
85
+ size = 'medium',
86
+ color = 'primary',
87
+ variant = 'outlined',
88
+ layout = 'list',
89
+ error = false,
66
90
  } = props
67
91
 
68
- const classes = withState({ hidden, disabled, selected, image: Boolean(image) })
69
-
70
- const handleClick = (event: React.MouseEvent<HTMLElement>) => onClick?.(event, value)
92
+ const classes = withState({
93
+ disabled,
94
+ selected,
95
+ image: Boolean(image),
96
+ variant,
97
+ size,
98
+ color,
99
+ layout,
100
+ error,
101
+ })
71
102
 
72
103
  return (
73
- <ButtonBase
74
- component='div'
104
+ <RenderComponent
75
105
  className={classes.root}
76
- onClick={handleClick}
106
+ onClick={onClick && ((event) => onClick?.(event, value))}
77
107
  disabled={disabled}
78
108
  sx={[
79
109
  (theme) => ({
80
- display: 'grid',
81
- width: '100%',
82
- gridTemplateColumns: 'min-content auto auto',
83
- gridTemplateAreas: `
84
- "image title action"
85
- "image details ${price ? 'price' : 'details'}"
86
- "image secondaryAction additionalDetails"
87
- "after after after"
88
- `,
89
- justifyContent: 'unset',
90
- typography: 'body1',
91
- // textAlign: 'left',
92
- background: theme.palette.background.paper,
93
- padding: `calc(${theme.spacings.xxs} + 1px) calc(${theme.spacings.xs} + 1px)`,
94
- columnGap: theme.spacings.xxs,
95
- border: `1px solid ${theme.palette.divider}`,
96
- borderBottomColor: `transparent`,
97
- '&:first-of-type': {
98
- ...breakpointVal(
99
- 'borderTopLeftRadius',
100
- theme.shape.borderRadius * 3,
101
- theme.shape.borderRadius * 4,
102
- theme.breakpoints.values,
103
- ),
104
- ...breakpointVal(
105
- 'borderTopRightRadius',
106
- theme.shape.borderRadius * 3,
107
- theme.shape.borderRadius * 4,
108
- theme.breakpoints.values,
109
- ),
110
+ '&.sizeSmall': {
111
+ padding: `5px 10px`,
112
+ display: 'flex',
113
+ typography: 'body2',
110
114
  },
111
- '&:last-of-type': {
112
- ...breakpointVal(
113
- 'borderBottomLeftRadius',
114
- theme.shape.borderRadius * 3,
115
- theme.shape.borderRadius * 4,
116
- theme.breakpoints.values,
117
- ),
118
- ...breakpointVal(
119
- 'borderBottomRightRadius',
120
- theme.shape.borderRadius * 3,
121
- theme.shape.borderRadius * 4,
122
- theme.breakpoints.values,
123
- ),
124
- borderBottom: `1px solid ${theme.palette.divider}`,
115
+
116
+ '&.sizeMedium': {
117
+ padding: `10px 12px`,
118
+ typography: 'body2',
119
+ display: 'block',
125
120
  },
126
- }),
127
- !image && {
128
- gridTemplateColumns: 'auto auto',
129
- gridTemplateAreas: `
130
- "title action"
131
- "details ${price ? 'price' : 'details'}"
132
- "secondaryAction additionalDetails"
133
- "after after"
134
- `,
135
- },
136
- hidden && {
137
- display: 'none',
138
- },
139
- selected &&
140
- ((theme) => ({
141
- border: `2px solid ${theme.palette.secondary.main} !important`,
142
- boxShadow: `0 0 0 4px ${alpha(
143
- theme.palette.secondary.main,
144
- theme.palette.action.hoverOpacity,
145
- )} !important`,
146
- ...breakpointVal(
147
- 'borderRadius',
148
- theme.shape.borderRadius * 3,
149
- theme.shape.borderRadius * 4,
150
- theme.breakpoints.values,
151
- ),
121
+
122
+ '&.sizeLarge': {
152
123
  padding: `${theme.spacings.xxs} ${theme.spacings.xs}`,
153
- })),
154
- disabled &&
155
- ((theme) => ({
124
+ display: 'block',
125
+ },
126
+
127
+ '&.variantDefault': {
128
+ borderBottom: `1px solid ${theme.palette.divider}`,
129
+ '&.selected': {
130
+ borderBottom: `2px solid ${theme.palette[color].main}`,
131
+ marginBottom: '-1px',
132
+ backgroundColor: `${theme.palette[color].main}10`,
133
+ },
134
+ '&.error': {
135
+ borderBottom: `2px solid ${theme.palette.error.main}`,
136
+ marginBottom: '-1px',
137
+ backgroundColor: `${theme.palette.error.main}10`,
138
+ },
139
+ },
140
+
141
+ '&.variantOutlined': {
142
+ backgroundColor: theme.palette.background.paper,
143
+ border: `1px solid ${theme.palette.divider}`,
144
+ '&:not(:last-of-type)': {
145
+ marginBottom: '-1px',
146
+ },
147
+
148
+ '&.layoutList': {
149
+ '&:first-of-type, &.selected': {
150
+ ...breakpointVal(
151
+ 'borderTopLeftRadius',
152
+ theme.shape.borderRadius * 3,
153
+ theme.shape.borderRadius * 4,
154
+ theme.breakpoints.values,
155
+ ),
156
+ ...breakpointVal(
157
+ 'borderTopRightRadius',
158
+ theme.shape.borderRadius * 3,
159
+ theme.shape.borderRadius * 4,
160
+ theme.breakpoints.values,
161
+ ),
162
+ },
163
+ '&:last-of-type, &.selected': {
164
+ ...breakpointVal(
165
+ 'borderBottomLeftRadius',
166
+ theme.shape.borderRadius * 3,
167
+ theme.shape.borderRadius * 4,
168
+ theme.breakpoints.values,
169
+ ),
170
+ ...breakpointVal(
171
+ 'borderBottomRightRadius',
172
+ theme.shape.borderRadius * 3,
173
+ theme.shape.borderRadius * 4,
174
+ theme.breakpoints.values,
175
+ ),
176
+ },
177
+ },
178
+ '&:not(.layoutList)': {
179
+ ...breakpointVal(
180
+ 'borderRadius',
181
+ theme.shape.borderRadius * 3,
182
+ theme.shape.borderRadius * 4,
183
+ theme.breakpoints.values,
184
+ ),
185
+ },
186
+
187
+ '&.selected': {
188
+ border: `2px solid ${theme.palette[color].main}`,
189
+ boxShadow: `0 0 0 4px ${alpha(
190
+ theme.palette[color].main,
191
+ theme.palette.action.hoverOpacity,
192
+ )}`,
193
+
194
+ '&.sizeSmall': { padding: `4px 9px` },
195
+ '&.sizeMedium': { padding: `9px 11px` },
196
+ '&.sizeLarge': {
197
+ padding: `calc(${theme.spacings.xxs} - 1px) calc(${theme.spacings.xs} - 1px)`,
198
+ },
199
+ },
200
+ '&.error': {
201
+ border: `2px solid ${theme.palette.error.main}`,
202
+ },
203
+ },
204
+ '&.selected': {
205
+ zIndex: 1,
206
+ },
207
+ '&.disabled': {
156
208
  '& *': {
157
209
  opacity: theme.palette.action.disabledOpacity,
158
210
  },
@@ -160,64 +212,96 @@ export function ActionCard(props: ActionCardProps) {
160
212
  theme.palette.action.disabledBackground,
161
213
  theme.palette.action.disabledOpacity / 10,
162
214
  ),
163
- })),
164
-
215
+ },
216
+ }),
165
217
  ...(Array.isArray(sx) ? sx : [sx]),
166
218
  ]}
167
219
  >
168
- {image && (
220
+ <Box
221
+ sx={{
222
+ display: 'flex',
223
+ flexDirection: 'row',
224
+ width: '100%',
225
+ justifyContent: 'space-between',
226
+ }}
227
+ >
169
228
  <Box
170
- className={classes.image}
171
229
  sx={{
172
- gridArea: 'image',
173
230
  display: 'flex',
231
+ flexDirection: 'row',
232
+ justifyContent: 'space-between',
174
233
  }}
175
234
  >
176
- {image}
177
- </Box>
178
- )}
179
- {title && (
180
- <Box className={classes.title} sx={{ gridArea: 'title', display: 'flex' }}>
181
- {title}
182
- </Box>
183
- )}
184
- {action && (
185
- <Box className={classes.action} sx={{ gridArea: 'action', textAlign: 'right' }}>
186
- {!selected ? action : reset}
187
- </Box>
188
- )}
189
- {details && (
190
- <Box
191
- className={classes.details}
192
- sx={(theme) => ({
193
- typography: 'body2',
194
- gridArea: 'details',
195
- color: 'text.secondary',
196
- })}
197
- >
198
- {details}
235
+ {image && (
236
+ <Box
237
+ className={classes.image}
238
+ sx={{ display: 'flex', paddingRight: '15px', alignSelf: 'center' }}
239
+ >
240
+ {image}
241
+ </Box>
242
+ )}
243
+
244
+ <Box
245
+ sx={{
246
+ display: 'flex',
247
+ justifyContent: 'center',
248
+ flexDirection: 'column',
249
+ alignItems: 'flex-start',
250
+ }}
251
+ >
252
+ {title && (
253
+ <Box
254
+ className={classes.title}
255
+ sx={{
256
+ typography: 'subtitle2',
257
+ '&.sizeMedium': { typographty: 'subtitle1' },
258
+ '&.sizeLarge': { typography: 'h6' },
259
+ }}
260
+ >
261
+ {title}
262
+ </Box>
263
+ )}
264
+
265
+ {details && (
266
+ <Box className={classes.details} sx={{ color: 'text.secondary' }}>
267
+ {details}
268
+ </Box>
269
+ )}
270
+
271
+ {secondaryAction && <Box className={classes.secondaryAction}>{secondaryAction}</Box>}
272
+ </Box>
199
273
  </Box>
200
- )}
201
274
 
202
- {price && !disabled && (
203
275
  <Box
204
- className={classes.price}
205
- sx={{ gridArea: 'price', textAlign: 'right', typography: 'h5' }}
276
+ sx={{
277
+ display: 'flex',
278
+ flexDirection: 'column',
279
+ justifyContent: 'space-between',
280
+ alignItems: 'flex-end',
281
+ }}
206
282
  >
207
- {price}
208
- </Box>
209
- )}
283
+ {action && (
284
+ <Box className={classes.action} sx={{ marginBottom: '5px' }}>
285
+ {!selected ? action : reset}
286
+ </Box>
287
+ )}
210
288
 
211
- {secondaryAction && (
212
- <Box className={classes.secondaryAction} sx={{ gridArea: 'secondaryAction' }}>
213
- {secondaryAction}
214
- </Box>
215
- )}
216
- {after && (
217
- <Box className={classes.after} sx={{ gridArea: 'after' }}>
218
- {after}
289
+ {price && !disabled && (
290
+ <Box
291
+ className={classes.price}
292
+ sx={{
293
+ textAlign: 'right',
294
+ typography: 'body1',
295
+ '&.sizeMedium': { typographty: 'subtitle1' },
296
+ '&.sizeLarge': { typography: 'h6' },
297
+ }}
298
+ >
299
+ {price}
300
+ </Box>
301
+ )}
219
302
  </Box>
220
- )}
221
- </ButtonBase>
303
+ </Box>
304
+ {after && <Box className={classes.after}>{after}</Box>}
305
+ </RenderComponent>
222
306
  )
223
307
  }
@@ -1,10 +1,12 @@
1
- import { Alert, Box } from '@mui/material'
1
+ import { Alert, Box, SxProps, Theme } from '@mui/material'
2
2
  import React from 'react'
3
3
  import { isFragment } from 'react-is'
4
+ import { extendableComponent } from '../Styles'
4
5
  import { ActionCardProps } from './ActionCard'
5
6
 
6
7
  type MultiSelect = {
7
8
  multiple: true
9
+ collapse?: false
8
10
  value: (string | number)[]
9
11
 
10
12
  onChange?: (event: React.MouseEvent<HTMLElement>, value: MultiSelect['value']) => void
@@ -12,6 +14,7 @@ type MultiSelect = {
12
14
  type Select = {
13
15
  multiple?: false
14
16
  value: string | number
17
+ collapse?: boolean
15
18
 
16
19
  /** Value is null when deselected when not required */
17
20
  onChange?: (event: React.MouseEvent<HTMLElement>, value: Select['value'] | null) => void
@@ -22,7 +25,9 @@ export type ActionCardListProps<SelectOrMulti = MultiSelect | Select> = {
22
25
  required?: boolean
23
26
  error?: boolean
24
27
  errorMessage?: string
25
- } & SelectOrMulti
28
+ sx?: SxProps<Theme>
29
+ } & SelectOrMulti &
30
+ HoistedActionCardProps
26
31
 
27
32
  function isMulti(props: ActionCardListProps): props is ActionCardListProps<MultiSelect> {
28
33
  return props.multiple === true
@@ -37,9 +42,30 @@ function isValueSelected(
37
42
  return value === candidate
38
43
  }
39
44
 
45
+ type HoistedActionCardProps = Pick<ActionCardProps, 'color' | 'variant' | 'size' | 'layout'>
46
+
47
+ const parts = ['root'] as const
48
+ const name = 'ActionCardList'
49
+ const { withState, selectors } = extendableComponent<
50
+ HoistedActionCardProps,
51
+ typeof name,
52
+ typeof parts
53
+ >(name, parts)
54
+
40
55
  export const ActionCardList = React.forwardRef<HTMLDivElement, ActionCardListProps>(
41
56
  (props, ref) => {
42
- const { children, required, error = false, errorMessage } = props
57
+ const {
58
+ children,
59
+ required,
60
+ error = false,
61
+ errorMessage,
62
+ size = 'medium',
63
+ color = 'primary',
64
+ variant = 'outlined',
65
+ layout = 'list',
66
+ collapse = false,
67
+ sx = [],
68
+ } = props
43
69
 
44
70
  const handleChange: ActionCardProps['onClick'] = isMulti(props)
45
71
  ? (event, v) => {
@@ -65,8 +91,10 @@ export const ActionCardList = React.forwardRef<HTMLDivElement, ActionCardListPro
65
91
  }
66
92
 
67
93
  type ActionCardLike = React.ReactElement<
68
- Pick<ActionCardProps, 'value' | 'selected' | 'disabled' | 'onClick' | 'hidden'>
94
+ Pick<ActionCardProps, 'value' | 'selected' | 'disabled' | 'onClick' | 'error' | 'onClick'> &
95
+ HoistedActionCardProps
69
96
  >
97
+
70
98
  function isActionCardLike(el: React.ReactElement): el is ActionCardLike {
71
99
  const hasValue = (el as ActionCardLike).props.value
72
100
 
@@ -100,55 +128,68 @@ export const ActionCardList = React.forwardRef<HTMLDivElement, ActionCardListPro
100
128
  (child) => child.props.value === props.value && child.props.disabled !== true,
101
129
  )?.props.value
102
130
 
131
+ const classes = withState({ size, color, variant, layout })
132
+
103
133
  return (
104
- <Box
105
- ref={ref}
106
- sx={[
107
- error && {
108
- '& .ActionCard-root': {
109
- borderLeft: 2,
110
- borderRight: 2,
111
- borderLeftColor: 'error.main',
112
- borderRightColor: 'error.main',
113
- },
114
- '& > div:first-of-type.ActionCard-root': {
115
- borderTop: 2,
116
- borderTopColor: 'error.main',
117
- },
118
- '& > div:last-of-type.ActionCard-root': {
119
- borderBottom: 2,
120
- borderBottomColor: 'error.main',
121
- },
122
- },
123
- ]}
124
- >
125
- {childReactNodes.map((child) =>
126
- React.cloneElement(child, {
127
- onClick: handleChange,
128
- hidden: !!value && value !== child.props.value,
129
- selected:
130
- child.props.selected === undefined
131
- ? isValueSelected(child.props.value, value)
132
- : child.props.selected,
133
- }),
134
- )}
135
- {error && (
136
- <Box component='span'>
137
- <Alert
138
- severity='error'
139
- variant='standard'
140
- sx={(theme) => ({
141
- marginTop: 0.5,
142
- borderStartStartRadius: 0,
143
- borderStartEndRadius: 0,
144
- borderRadius: theme.shape.borderRadius * 1,
145
- })}
146
- >
147
- {errorMessage}
148
- </Alert>
149
- </Box>
134
+ <div>
135
+ <Box
136
+ className={classes.root}
137
+ ref={ref}
138
+ sx={[
139
+ (theme) => ({
140
+ '&.layoutStack': {
141
+ display: 'grid',
142
+ height: 'min-content',
143
+ gap: theme.spacings.xxs,
144
+ },
145
+ '&.layoutList': {
146
+ display: 'grid',
147
+ height: 'min-content',
148
+ },
149
+ '&.layoutGrid': {
150
+ display: 'grid',
151
+ gridTemplateColumns: 'repeat(2, 1fr)',
152
+ gap: theme.spacings.xxs,
153
+ },
154
+ '&.layoutInline': {
155
+ display: 'flex',
156
+ flexWrap: 'wrap',
157
+ gap: theme.spacings.xxs,
158
+ },
159
+ }),
160
+
161
+ ...(Array.isArray(sx) ? sx : [sx]),
162
+ ]}
163
+ >
164
+ {childReactNodes.map((child) => {
165
+ if (collapse && Boolean(value) && !isValueSelected(child.props.value, value))
166
+ return null
167
+ return React.cloneElement(child, {
168
+ onClick: handleChange,
169
+ error: child.props.error ?? error,
170
+ color: child.props.color ?? color,
171
+ variant: child.props.variant ?? variant,
172
+ size: child.props.size ?? size,
173
+ layout: child.props.layout ?? layout,
174
+ selected:
175
+ child.props.selected === undefined
176
+ ? isValueSelected(child.props.value, value)
177
+ : child.props.selected,
178
+ })
179
+ })}
180
+ </Box>
181
+ {error && errorMessage && (
182
+ <Alert
183
+ severity='error'
184
+ variant='standard'
185
+ sx={(theme) => ({
186
+ marginTop: theme.spacings.xxs,
187
+ })}
188
+ >
189
+ {errorMessage}
190
+ </Alert>
150
191
  )}
151
- </Box>
192
+ </div>
152
193
  )
153
194
  },
154
195
  )
@@ -6,37 +6,37 @@ import { ActionCardList, ActionCardListProps } from './ActionCardList'
6
6
 
7
7
  export type ActionCardItemBase = Pick<ActionCardProps, 'value'>
8
8
 
9
- export type ActionCardItemRenderProps<T> = Pick<
10
- ActionCardProps,
11
- 'selected' | 'hidden' | 'value'
12
- > & {
9
+ export type ActionCardItemRenderProps<T> = ActionCardProps & {
13
10
  onReset: MouseEventHandler<HTMLAnchorElement> & MouseEventHandler<HTMLSpanElement>
14
11
  } & T
15
12
 
16
13
  export type ActionCardListFormProps<T extends ActionCardItemBase> = Omit<
17
14
  ActionCardListProps,
18
- 'value'
15
+ 'value' | 'error' | 'onChange' | 'children' | 'multiple'
19
16
  > &
20
- Omit<ControllerProps<any>, 'render'> & {
17
+ Omit<ControllerProps<any>, 'render' | 'shouldUnregister'> & {
21
18
  items: T[]
22
- render: React.VFC<ActionCardItemRenderProps<T>>
19
+ render: React.FC<ActionCardItemRenderProps<T>>
23
20
  }
24
21
 
25
22
  export function ActionCardListForm<T extends ActionCardItemBase>(
26
23
  props: ActionCardListFormProps<T>,
27
24
  ) {
28
- const { required, rules, items, render, control, name, errorMessage } = props
29
- const RenderItem = render as React.VFC<ActionCardItemRenderProps<ActionCardItemBase>>
25
+ const { required, rules, items, render, control, name, errorMessage, defaultValue, ...other } =
26
+ props
27
+ const RenderItem = render as React.FC<ActionCardItemRenderProps<ActionCardItemBase>>
30
28
 
31
29
  return (
32
30
  <Controller
33
31
  {...props}
34
32
  control={control}
35
33
  name={name}
36
- rules={{ required, ...rules, validate: (v) => (v ? true : errorMessage) }}
34
+ defaultValue={defaultValue}
35
+ rules={{ required: errorMessage || required, ...rules }}
37
36
  render={({ field: { onChange, value, ref }, fieldState, formState }) => (
38
37
  <ActionCardList
39
- required
38
+ {...other}
39
+ required={required}
40
40
  value={value}
41
41
  ref={ref}
42
42
  onChange={(_, incomming) => onChange(incomming)}
package/Button/Button.tsx CHANGED
@@ -1,5 +1,9 @@
1
1
  /* eslint-disable react/forbid-foreign-prop-types */
2
- import { LoadingButton as Button, LoadingButtonProps } from '@mui/lab'
2
+ import { LoadingButton as Button, LoadingButtonProps, LoadingButtonTypeMap } from '@mui/lab'
3
+
4
+ export type ButtonProps<
5
+ D extends React.ElementType = LoadingButtonTypeMap['defaultComponent'],
6
+ P = {},
7
+ > = LoadingButtonProps<D, P>
3
8
 
4
- export type ButtonProps = LoadingButtonProps
5
9
  export { Button }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.28.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1662](https://github.com/graphcommerce-org/graphcommerce/pull/1662) [`0c21c5c23`](https://github.com/graphcommerce-org/graphcommerce/commit/0c21c5c233ebab15f6629c234e3de1cc8c0452e1) Thanks [@paales](https://github.com/paales)! - Implement serverRenderDepth prop to the Navigation to limit initial render time and TBT
8
+
9
+ * [#1662](https://github.com/graphcommerce-org/graphcommerce/pull/1662) [`f5eae0afd`](https://github.com/graphcommerce-org/graphcommerce/commit/f5eae0afdbd474b1f81c450425ffadf2d025187a) Thanks [@paales](https://github.com/paales)! - Move to useMatchMedia to have a simple boolean utility that allows to match to a certain breakpoint
10
+
11
+ ### Patch Changes
12
+
13
+ - [#1662](https://github.com/graphcommerce-org/graphcommerce/pull/1662) [`de8925aa9`](https://github.com/graphcommerce-org/graphcommerce/commit/de8925aa910b191c62041530c68c697a58a1e52d) Thanks [@paales](https://github.com/paales)! - Allow for a custom Component for magentoMenuToNavigation and allow React.ReactNode for items
14
+
15
+ - Updated dependencies [[`f5eae0afd`](https://github.com/graphcommerce-org/graphcommerce/commit/f5eae0afdbd474b1f81c450425ffadf2d025187a), [`9e0ca73eb`](https://github.com/graphcommerce-org/graphcommerce/commit/9e0ca73eb50ded578f4a98e40a7eb920bf8ab421)]:
16
+ - @graphcommerce/framer-scroller@2.1.40
17
+ - @graphcommerce/framer-next-pages@3.3.1
18
+
19
+ ## 4.27.0
20
+
21
+ ### Minor Changes
22
+
23
+ - [#1642](https://github.com/graphcommerce-org/graphcommerce/pull/1642) [`ad63ebf4e`](https://github.com/graphcommerce-org/graphcommerce/commit/ad63ebf4e33bfb0e5c9e5e68ab69b14775f3f8a8) Thanks [@paales](https://github.com/paales)! - Introduced `<AddProductsToCartForm/>`, which is allows for adding all product types to the cart with a single react-hook-form form.
24
+
25
+ Which allows you to fully compose the form on the product page without having to modify the page.
26
+
27
+ ### Patch Changes
28
+
29
+ - Updated dependencies []:
30
+ - @graphcommerce/framer-scroller@2.1.39
31
+
3
32
  ## 4.26.0
4
33
 
5
34
  ### Minor Changes
@@ -107,9 +107,8 @@ export function SidebarGallery(props: SidebarGalleryProps) {
107
107
 
108
108
  const headerHeight = `${theme.appShell.headerHeightSm} - ${theme.spacings.sm} * 2`
109
109
  const galleryMargin = theme.spacings.lg
110
- const extraSpacing = theme.spacings.md
111
110
 
112
- const maxHeight = `calc(100vh - ${headerHeight} - ${galleryMargin} - ${extraSpacing})`
111
+ const maxHeight = `calc(100vh - ${headerHeight} - ${galleryMargin})`
113
112
  const ratio = `calc(${height} / ${width} * 100%)`
114
113
 
115
114
  const hasImages = images.length > 0
@@ -200,7 +199,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
200
199
  width={image.width}
201
200
  height={image.height}
202
201
  loading={idx === 0 ? 'eager' : 'lazy'}
203
- sx={{ display: 'block' }}
202
+ sx={{ display: 'block', objectFit: 'contain' }}
204
203
  sizes={{
205
204
  0: '100vw',
206
205
  [theme.breakpoints.values.md]: zoomed ? '100vw' : '60vw',
@@ -1,18 +1,11 @@
1
1
  /* eslint-disable @typescript-eslint/no-use-before-define */
2
2
  import { useMotionValueValue } from '@graphcommerce/framer-utils'
3
- import {
4
- Box,
5
- ListItemButton,
6
- styled,
7
- Theme,
8
- useEventCallback,
9
- useMediaQuery,
10
- alpha,
11
- } from '@mui/material'
3
+ import { alpha, Box, ListItemButton, styled, useEventCallback, useTheme } from '@mui/material'
12
4
  import PageLink from 'next/link'
13
5
  import React from 'react'
14
6
  import { IconSvg } from '../../IconSvg'
15
7
  import { extendableComponent } from '../../Styles/extendableComponent'
8
+ import { useMatchMedia } from '../../hooks'
16
9
  import { iconChevronRight } from '../../icons'
17
10
  import {
18
11
  isNavigationButton,
@@ -54,9 +47,9 @@ const NavigationLI = styled('li')({ display: 'contents' })
54
47
 
55
48
  export const NavigationItem = React.memo<NavigationItemProps>((props) => {
56
49
  const { id, parentPath, idx, first, last, NavigationList, mouseEvent } = props
50
+ const { selection, hideRootOnNavigate, closing, animating, serverRenderDepth } = useNavigation()
57
51
 
58
52
  const row = idx + 1
59
- const { selection, hideRootOnNavigate, closing, animating } = useNavigation()
60
53
 
61
54
  const itemPath = [...(parentPath ? parentPath.split(',') : []), id]
62
55
 
@@ -83,13 +76,18 @@ export const NavigationItem = React.memo<NavigationItemProps>((props) => {
83
76
  closing.set(true)
84
77
  })
85
78
 
86
- const isDesktop = useMediaQuery<Theme>((theme) => theme.breakpoints.up('md'))
79
+ const matchMedia = useMatchMedia()
87
80
 
88
81
  if (isNavigationButton(props)) {
89
- const { childItems, name } = props
82
+ const { childItems, name, href } = props
83
+
84
+ const skipChildren = itemPath.length + 1 > serverRenderDepth && !isSelected && !!href
85
+
90
86
  return (
91
87
  <NavigationLI className={classes.li}>
92
88
  <ListItemButton
89
+ component={href ? 'a' : 'div'}
90
+ href={href || undefined}
93
91
  className={classes.item}
94
92
  role='button'
95
93
  sx={[
@@ -116,14 +114,14 @@ export const NavigationItem = React.memo<NavigationItemProps>((props) => {
116
114
  tabIndex={tabIndex}
117
115
  onClick={(e) => {
118
116
  e.preventDefault()
119
- if (!isSelected && animating.get() === false) {
117
+ if (!isSelected && !animating.get()) {
120
118
  selection.set(itemPath)
121
119
  }
122
120
  }}
123
121
  onMouseMove={
124
- itemPath.length > 1 && mouseEvent === 'hover'
122
+ (itemPath.length > 1 || !hideRootOnNavigate) && mouseEvent === 'hover'
125
123
  ? (e) => {
126
- if (isDesktop && animating.get() === false && !isSelected) {
124
+ if (!isSelected && !animating.get() && matchMedia.up('md')) {
127
125
  e.preventDefault()
128
126
  setTimeout(() => selection.set(itemPath), 0)
129
127
  }
@@ -145,18 +143,21 @@ export const NavigationItem = React.memo<NavigationItemProps>((props) => {
145
143
  <IconSvg src={iconChevronRight} sx={{ flexShrink: 0 }} />
146
144
  </ListItemButton>
147
145
 
148
- <NavigationList
149
- items={childItems}
150
- selected={isSelected}
151
- parentPath={itemPath.join(',')}
152
- mouseEvent={mouseEvent}
153
- />
146
+ {!skipChildren && (
147
+ <NavigationList
148
+ items={childItems}
149
+ selected={isSelected}
150
+ parentPath={itemPath.join(',')}
151
+ mouseEvent={mouseEvent}
152
+ />
153
+ )}
154
154
  </NavigationLI>
155
155
  )
156
156
  }
157
157
 
158
158
  if (isNavigationHref(props)) {
159
159
  const { name, href } = props
160
+
160
161
  return (
161
162
  <NavigationLI sx={[hideItem && { display: 'none' }]} className={classes.li}>
162
163
  <PageLink href={href} passHref prefetch={false}>
@@ -198,8 +199,6 @@ export const NavigationItem = React.memo<NavigationItemProps>((props) => {
198
199
  )
199
200
  }
200
201
 
201
- if (process.env.NODE_ENV !== 'production') throw Error('NavigationItem: unknown type')
202
-
203
202
  return null
204
203
  })
205
204
 
@@ -11,6 +11,7 @@ import { LayoutTitle } from '../../Layout/components/LayoutTitle'
11
11
  import { Overlay } from '../../Overlay/components/Overlay'
12
12
  import { extendableComponent } from '../../Styles/extendableComponent'
13
13
  import { useFabSize } from '../../Theme'
14
+ import { useMatchMedia } from '../../hooks'
14
15
  import { iconClose, iconChevronLeft } from '../../icons'
15
16
  import { useNavigation } from '../hooks/useNavigation'
16
17
  import { mouseEventPref } from './NavigationItem'
@@ -57,14 +58,15 @@ export const NavigationOverlay = React.memo<NavigationOverlayProps>((props) => {
57
58
  mouseEvent,
58
59
  itemPadding = 'md',
59
60
  } = props
60
- const { selection, items, animating, closing } = useNavigation()
61
+ const { selection, items, animating, closing, serverRenderDepth } = useNavigation()
61
62
 
62
63
  const fabSize = useFabSize('responsive')
63
64
  const svgSize = useIconSvgSize('large')
64
65
 
65
- const theme2 = useTheme()
66
+ const matchMedia = useMatchMedia()
67
+
66
68
  const handleOnBack = useEventCallback(() => {
67
- if (window.matchMedia(`(max-width: ${theme2.breakpoints.values.md}px)`).matches) {
69
+ if (matchMedia.down('md')) {
68
70
  const current = selection.get()
69
71
  selection.set(current !== false ? current.slice(0, -1) : false)
70
72
  } else selection.set([])
@@ -87,6 +89,8 @@ export const NavigationOverlay = React.memo<NavigationOverlayProps>((props) => {
87
89
 
88
90
  const handleClose = useEventCallback(() => closing.set(true))
89
91
 
92
+ if (selectedLevel === -1 && serverRenderDepth <= 0) return null
93
+
90
94
  return (
91
95
  <Overlay
92
96
  className={classes.root}
@@ -6,6 +6,8 @@ import {
6
6
  NavigationContextType,
7
7
  NavigationContext,
8
8
  UseNavigationSelection,
9
+ NavigationNodeType,
10
+ NavigationNodeComponent,
9
11
  } from '../hooks/useNavigation'
10
12
 
11
13
  export type NavigationProviderProps = {
@@ -15,6 +17,7 @@ export type NavigationProviderProps = {
15
17
  children?: React.ReactNode
16
18
  animationDuration?: number
17
19
  selection: UseNavigationSelection
20
+ serverRenderDepth?: number
18
21
  }
19
22
 
20
23
  const nonNullable = <T,>(value: T): value is NonNullable<T> => value !== null && value !== undefined
@@ -27,6 +30,7 @@ export const NavigationProvider = React.memo<NavigationProviderProps>((props) =>
27
30
  animationDuration = 0.225,
28
31
  children,
29
32
  selection,
33
+ serverRenderDepth = 2,
30
34
  } = props
31
35
 
32
36
  const animating = useMotionValue(false)
@@ -39,10 +43,19 @@ export const NavigationProvider = React.memo<NavigationProviderProps>((props) =>
39
43
  animating,
40
44
  closing,
41
45
  items: items
42
- .map((item, index) => (isElement(item) ? { id: item.key ?? index, component: item } : item))
46
+ .map((item, index) =>
47
+ isElement(item)
48
+ ? ({
49
+ type: NavigationNodeType.COMPONENT,
50
+ id: item.key ?? index,
51
+ component: item,
52
+ } as NavigationNodeComponent)
53
+ : item,
54
+ )
43
55
  .filter(nonNullable),
56
+ serverRenderDepth,
44
57
  }),
45
- [hideRootOnNavigate, selection, animating, closing, items],
58
+ [hideRootOnNavigate, selection, animating, closing, items, serverRenderDepth],
46
59
  )
47
60
 
48
61
  return (
@@ -1,5 +1,5 @@
1
1
  import { MotionValue, useMotionValue } from 'framer-motion'
2
- import { createContext, MutableRefObject, useContext } from 'react'
2
+ import React, { createContext, MutableRefObject, useContext } from 'react'
3
3
 
4
4
  export type NavigationId = string | number
5
5
  export type NavigationPath = NavigationId[]
@@ -19,38 +19,51 @@ export type NavigationContextType = {
19
19
  hideRootOnNavigate: boolean
20
20
  animating: MotionValue<boolean>
21
21
  closing: MotionValue<boolean>
22
+ serverRenderDepth: number
22
23
  }
23
24
 
24
25
  type NavigationNodeBase = {
26
+ type?: NavigationNodeType
25
27
  id: NavigationId
26
28
  }
27
29
 
30
+ export enum NavigationNodeType {
31
+ LINK,
32
+ BUTTON,
33
+ COMPONENT,
34
+ }
35
+
28
36
  export type NavigationNodeHref = NavigationNodeBase & {
29
- name: string
37
+ name: React.ReactNode
30
38
  href: string
31
39
  }
32
40
 
33
41
  export type NavigationNodeButton = NavigationNodeBase & {
34
- name: string
42
+ name: React.ReactNode
43
+ type: NavigationNodeType.BUTTON
44
+ href?: string
35
45
  childItems: NavigationNode[]
36
46
  }
37
47
 
38
48
  export type NavigationNodeComponent = NavigationNodeBase & {
49
+ type: NavigationNodeType.COMPONENT
39
50
  component: React.ReactNode
40
51
  }
41
52
 
42
53
  export type NavigationNode = NavigationNodeHref | NavigationNodeButton | NavigationNodeComponent
43
54
 
44
55
  export function isNavigationHref(node: NavigationNodeBase): node is NavigationNodeHref {
45
- return 'href' in node
56
+ return 'href' in node && node.type !== NavigationNodeType.BUTTON
46
57
  }
47
58
 
48
59
  export function isNavigationButton(node: NavigationNodeBase): node is NavigationNodeButton {
49
- return (node as NavigationNodeButton).childItems?.length > 0
60
+ return (
61
+ node.type === NavigationNodeType.BUTTON && (node as NavigationNodeButton).childItems?.length > 0
62
+ )
50
63
  }
51
64
 
52
65
  export function isNavigationComponent(node: NavigationNodeBase): node is NavigationNodeComponent {
53
- return 'component' in node
66
+ return node.type === NavigationNodeType.COMPONENT && 'component' in node
54
67
  }
55
68
 
56
69
  export const NavigationContext = createContext(undefined as unknown as NavigationContextType)
@@ -42,6 +42,13 @@ export function RenderType<
42
42
  export function findByTypename<T extends TypeObject, Typename extends T['__typename']>(
43
43
  type: (T | undefined | null)[] | undefined | null,
44
44
  typename: Typename,
45
- ): FilterTypeByTypename<T, Typename> {
45
+ ): FilterTypeByTypename<T, Typename> | undefined {
46
46
  return type?.find((item) => item?.__typename === typename) as FilterTypeByTypename<T, Typename>
47
47
  }
48
+
49
+ export function isTypename<T extends TypeObject, Typenames extends T['__typename'][]>(
50
+ type: FilterTypeByTypename<T, T['__typename']>,
51
+ typename: Typenames,
52
+ ): type is FilterTypeByTypename<T, Typenames[number]> {
53
+ return typename.includes(type.__typename)
54
+ }
@@ -3,7 +3,7 @@ import type { OptionalKeysOf, Simplify } from 'type-fest'
3
3
  export function filterNonNullableKeys<
4
4
  T extends Record<string, unknown>,
5
5
  Keys extends OptionalKeysOf<T>,
6
- >(items: (T | null | undefined)[] | null | undefined, values: Keys[]) {
6
+ >(items: (T | null | undefined)[] | null | undefined, values: Keys[] = []) {
7
7
  if (!items) return []
8
8
 
9
9
  type ResultWithRequired = Simplify<
package/hooks/index.ts CHANGED
@@ -2,3 +2,4 @@ export * from './useDateTimeFormat'
2
2
  export * from './useNumberFormat'
3
3
  export * from './useUrlQuery'
4
4
  export * from './useMemoDeep'
5
+ export * from './useMatchMedia'
@@ -0,0 +1,17 @@
1
+ import { Breakpoint, useTheme } from '@mui/material'
2
+ import { useMemo } from 'react'
3
+
4
+ export function useMatchMedia() {
5
+ const theme = useTheme()
6
+
7
+ return useMemo(() => {
8
+ const callback = (direction: 'up' | 'down', breakpointKey: number | Breakpoint) =>
9
+ window.matchMedia(theme.breakpoints[direction](breakpointKey).replace(/^@media( ?)/m, ''))
10
+ .matches
11
+
12
+ return {
13
+ down: (key: number | Breakpoint) => callback('down', key),
14
+ up: (key: number | Breakpoint) => callback('up', key),
15
+ }
16
+ }, [theme.breakpoints])
17
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/next-ui",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "4.26.0",
5
+ "version": "4.28.0",
6
6
  "author": "",
7
7
  "license": "MIT",
8
8
  "sideEffects": false,
@@ -19,8 +19,8 @@
19
19
  "@emotion/react": "^11.9.3",
20
20
  "@emotion/server": "^11.4.0",
21
21
  "@emotion/styled": "^11.9.3",
22
- "@graphcommerce/framer-next-pages": "3.3.0",
23
- "@graphcommerce/framer-scroller": "2.1.38",
22
+ "@graphcommerce/framer-next-pages": "3.3.1",
23
+ "@graphcommerce/framer-scroller": "2.1.40",
24
24
  "@graphcommerce/framer-utils": "3.2.0",
25
25
  "@graphcommerce/image": "3.1.9",
26
26
  "cookie": "^0.5.0",