@graphcommerce/next-ui 4.13.0 → 4.15.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.
@@ -11,7 +11,7 @@ export type ActionCardProps = {
11
11
  price?: React.ReactNode
12
12
  after?: React.ReactNode
13
13
  secondaryAction?: React.ReactNode
14
- onClick?: (e: FormEvent<HTMLElement>, v: string | number) => void
14
+ onClick?: (event: React.MouseEvent<HTMLElement>, value: string | number) => void
15
15
  selected?: boolean
16
16
  hidden?: boolean
17
17
  value: string | number
@@ -66,7 +66,7 @@ export function ActionCard(props: ActionCardProps) {
66
66
 
67
67
  const classes = withState({ hidden, disabled, selected, image: Boolean(image) })
68
68
 
69
- const handleClick = (event: FormEvent<HTMLElement>) => onClick?.(event, value)
69
+ const handleClick = (event: React.MouseEvent<HTMLElement>) => onClick?.(event, value)
70
70
 
71
71
  return (
72
72
  <ButtonBase
@@ -3,19 +3,20 @@ import { AnimatePresence } from 'framer-motion'
3
3
  import React from 'react'
4
4
  import { isFragment } from 'react-is'
5
5
  import { AnimatedRow } from '../AnimatedRow/AnimatedRow'
6
+ import { ActionCardProps } from './ActionCard'
6
7
 
7
8
  type MultiSelect = {
8
9
  multiple: true
9
- value: string[]
10
+ value: (string | number)[]
10
11
 
11
- onChange?: (event: React.MouseEvent<HTMLElement>, value: string[]) => void
12
+ onChange?: (event: React.MouseEvent<HTMLElement>, value: MultiSelect['value']) => void
12
13
  }
13
14
  type Select = {
14
15
  multiple?: false
15
- value: string
16
+ value: string | number
16
17
 
17
18
  /** Value is null when deselected when not required */
18
- onChange?: (event: React.MouseEvent<HTMLElement>, value: string | null) => void
19
+ onChange?: (event: React.MouseEvent<HTMLElement>, value: Select['value'] | null) => void
19
20
  }
20
21
 
21
22
  export type ActionCardListProps<SelectOrMulti = MultiSelect | Select> = {
@@ -29,94 +30,124 @@ function isMulti(props: ActionCardListProps): props is ActionCardListProps<Multi
29
30
  return props.multiple === true
30
31
  }
31
32
 
32
- function isValueSelected(value: string, candidate: string | string[]) {
33
- if (candidate === undefined || value === undefined) return false
33
+ function isValueSelected(
34
+ value: ActionCardProps['value'],
35
+ candidate?: Select['value'] | MultiSelect['value'],
36
+ ) {
37
+ if (candidate === undefined) return false
34
38
  if (Array.isArray(candidate)) return candidate.indexOf(value) >= 0
35
39
  return value === candidate
36
40
  }
37
41
 
38
- export function ActionCardList(props: ActionCardListProps) {
39
- const { children, required, value, error = false, errorMessage } = props
42
+ export const ActionCardList = React.forwardRef<HTMLDivElement, ActionCardListProps>(
43
+ (props, ref) => {
44
+ const { children, required, error = false, errorMessage } = props
40
45
 
41
- const handleChange = isMulti(props)
42
- ? (event: React.MouseEvent<HTMLElement, MouseEvent>, buttonValue: string) => {
43
- const { onChange } = props
44
- const index = Boolean(value) && value?.indexOf(buttonValue)
45
- let newValue: string[]
46
+ const handleChange: ActionCardProps['onClick'] = isMulti(props)
47
+ ? (event, v) => {
48
+ const { onChange, value } = props
49
+ const index = Boolean(value) && value?.indexOf(v)
50
+ let newValue: typeof value
46
51
 
47
- if (Array.isArray(value) && value.length && index && index >= 0) {
48
- newValue = value.slice()
49
- newValue.splice(index, 1)
50
- } else {
51
- newValue = value ? [...value, buttonValue] : [buttonValue]
52
+ if (value.length && index && index >= 0) {
53
+ newValue = value.slice()
54
+ newValue.splice(index, 1)
55
+ } else {
56
+ newValue = value ? [...value, v] : [v]
57
+ }
58
+ onChange?.(event, newValue)
52
59
  }
53
- onChange?.(event, newValue)
54
- }
55
- : (event: React.MouseEvent<HTMLElement, MouseEvent>, buttonValue: string) => {
56
- const { onChange } = props
60
+ : (event, v) => {
61
+ const { onChange, value } = props
57
62
 
58
- if (value === buttonValue) return
59
- if (required) onChange?.(event, buttonValue)
60
- else onChange?.(event, value === buttonValue ? null : buttonValue)
61
- }
63
+ if (value !== v) {
64
+ if (required) onChange?.(event, v)
65
+ else onChange?.(event, value === v ? null : v)
66
+ }
67
+ }
62
68
 
63
- return (
64
- <Box
65
- sx={[
66
- error &&
67
- ((theme) => ({
68
- '& .ActionCard-root': {
69
- borderLeft: 2,
70
- borderRight: 2,
71
- borderLeftColor: 'error.main',
72
- borderRightColor: 'error.main',
73
- paddingLeft: theme.spacings.xs,
74
- paddingRight: theme.spacings.xs,
75
- },
76
- '& > div:first-of-type.ActionCard-root': {
77
- borderTop: 2,
78
- borderTopColor: 'error.main',
79
- paddingTop: theme.spacings.xxs,
80
- },
81
- '& > div:last-of-type.ActionCard-root': {
82
- borderBottom: 2,
83
- borderBottomColor: 'error.main',
84
- paddingBottom: theme.spacings.xxs,
85
- },
86
- })),
87
- ]}
69
+ type ActionCardLike = React.ReactElement<
70
+ Pick<ActionCardProps, 'value' | 'selected' | 'disabled' | 'onClick'>
88
71
  >
89
- {React.Children.map(children, (child) => {
90
- if (!React.isValidElement(child)) return null
72
+ function isActionCardLike(el: React.ReactElement): el is ActionCardLike {
73
+ const hasValue = (el as ActionCardLike).props.value
91
74
 
75
+ if (process.env.NODE_ENV !== 'production') {
76
+ if (!hasValue) console.error(el, `must be an instance of ActionCard`)
77
+ }
78
+ return (el as ActionCardLike).props.value !== undefined
79
+ }
80
+
81
+ // Make sure the children are cardlike
82
+ const childReactNodes = React.Children.toArray(children)
83
+ .filter(React.isValidElement)
84
+ .filter(isActionCardLike)
85
+ .filter((child) => {
92
86
  if (process.env.NODE_ENV !== 'production') {
93
- if (isFragment(child)) {
87
+ if (isFragment(child))
94
88
  console.error(
95
89
  [
96
90
  "@graphcommerce/next-ui: The ActionCardList component doesn't accept a Fragment as a child.",
97
- 'Consider providing an array instead.',
91
+ 'Consider providing an array instead',
98
92
  ].join('\n'),
99
93
  )
100
- }
101
94
  }
102
95
 
103
- return React.cloneElement(child, {
104
- onClick: handleChange,
105
- selected:
106
- child.props.selected === undefined
107
- ? isValueSelected(child.props.value as string, value)
108
- : child.props.selected,
109
- })
110
- })}
111
- {error && (
112
- <Alert
113
- severity='error'
114
- variant='filled'
115
- sx={{ borderStartStartRadius: 0, borderStartEndRadius: 0 }}
116
- >
117
- {errorMessage}
118
- </Alert>
119
- )}
120
- </Box>
121
- )
122
- }
96
+ return !isFragment(child)
97
+ })
98
+
99
+ // Make sure the selected values is in the list of all possible values
100
+ const value = childReactNodes.find(
101
+ // eslint-disable-next-line react/destructuring-assignment
102
+ (child) => child.props.value === props.value && child.props.disabled !== true,
103
+ )?.props.value
104
+
105
+ return (
106
+ <Box
107
+ ref={ref}
108
+ sx={[
109
+ error &&
110
+ ((theme) => ({
111
+ '& .ActionCard-root': {
112
+ borderLeft: 2,
113
+ borderRight: 2,
114
+ borderLeftColor: 'error.main',
115
+ borderRightColor: 'error.main',
116
+ paddingLeft: theme.spacings.xs,
117
+ paddingRight: theme.spacings.xs,
118
+ },
119
+ '& > div:first-of-type.ActionCard-root': {
120
+ borderTop: 2,
121
+ borderTopColor: 'error.main',
122
+ paddingTop: theme.spacings.xxs,
123
+ },
124
+ '& > div:last-of-type.ActionCard-root': {
125
+ borderBottom: 2,
126
+ borderBottomColor: 'error.main',
127
+ paddingBottom: theme.spacings.xxs,
128
+ },
129
+ })),
130
+ ]}
131
+ >
132
+ {childReactNodes.map((child) =>
133
+ React.cloneElement(child, {
134
+ onClick: handleChange,
135
+ selected:
136
+ child.props.selected === undefined
137
+ ? isValueSelected(child.props.value, value)
138
+ : child.props.selected,
139
+ }),
140
+ )}
141
+ {error && (
142
+ <Alert
143
+ severity='error'
144
+ variant='filled'
145
+ sx={{ borderStartStartRadius: 0, borderStartEndRadius: 0 }}
146
+ >
147
+ {errorMessage}
148
+ </Alert>
149
+ )}
150
+ </Box>
151
+ )
152
+ },
153
+ )
@@ -34,10 +34,11 @@ export function ActionCardListForm<T extends ActionCardItemBase>(
34
34
  control={control}
35
35
  name={name}
36
36
  rules={{ required, ...rules, validate: (v) => (v ? true : errorMessage) }}
37
- render={({ field: { onChange, value }, fieldState, formState }) => (
37
+ render={({ field: { onChange, value, onBlur, ref }, fieldState, formState }) => (
38
38
  <ActionCardList
39
39
  required
40
40
  value={value}
41
+ ref={ref}
41
42
  onChange={(_, incomming) => onChange(incomming)}
42
43
  error={formState.isSubmitted && !!fieldState.error}
43
44
  errorMessage={fieldState.error?.message}
@@ -20,7 +20,7 @@ const { classes } = extendableComponent(name, parts)
20
20
  export function BlogListItem(props: BlogListItemProps) {
21
21
  const { asset, url, date, title, sx = [] } = props
22
22
 
23
- const formatter = useDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' })
23
+ const formatter = useDateTimeFormat({ dateStyle: 'long' })
24
24
 
25
25
  return (
26
26
  <Box
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1566](https://github.com/graphcommerce-org/graphcommerce/pull/1566) [`e167992df`](https://github.com/graphcommerce-org/graphcommerce/commit/e167992dfdc6964a392af719667f8a188626ab1b) Thanks [@ErwinOtten](https://github.com/ErwinOtten)! - Introduced `@graphcommerce/next-ui/navigation` component.
8
+
9
+ - Navigation is always present in the DOM
10
+ - Configurable in LayoutNavigation.tsx
11
+ - Show categories directly, or nest them in a 'products' button
12
+ - Choose prefered mouseEvent: click or hover
13
+
14
+ * [#1566](https://github.com/graphcommerce-org/graphcommerce/pull/1566) [`9c2504b4e`](https://github.com/graphcommerce-org/graphcommerce/commit/9c2504b4ed75f41d3003c4d3339814010e85e37e) Thanks [@ErwinOtten](https://github.com/ErwinOtten)! - publish navigation
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies []:
19
+ - @graphcommerce/framer-scroller@2.1.25
20
+
21
+ ## 4.14.0
22
+
23
+ ### Minor Changes
24
+
25
+ - [#1553](https://github.com/graphcommerce-org/graphcommerce/pull/1553) [`323fdee4b`](https://github.com/graphcommerce-org/graphcommerce/commit/323fdee4b15ae23e0e84dd0588cb2c6446dcfd50) Thanks [@NickdeK](https://github.com/NickdeK)! - Added a new cookies utility to load cookies on the frontend
26
+
27
+ ### Patch Changes
28
+
29
+ - [#1553](https://github.com/graphcommerce-org/graphcommerce/pull/1553) [`afcd8e4bf`](https://github.com/graphcommerce-org/graphcommerce/commit/afcd8e4bfb7010da4d5faeed85b61991ed7975f4) Thanks [@NickdeK](https://github.com/NickdeK)! - ActionCardList will now show all options when the selected value isn't in any of the options
30
+
31
+ * [#1553](https://github.com/graphcommerce-org/graphcommerce/pull/1553) [`02e1988e5`](https://github.com/graphcommerce-org/graphcommerce/commit/02e1988e5f361c6f66ae30d3bbee38ef2ac062df) Thanks [@NickdeK](https://github.com/NickdeK)! - Make sure the useDateTimeFormat isn't giving hydration warnings
32
+
33
+ * Updated dependencies []:
34
+ - @graphcommerce/framer-scroller@2.1.24
35
+
36
+ ## 4.13.1
37
+
38
+ ### Patch Changes
39
+
40
+ - [#1552](https://github.com/graphcommerce-org/graphcommerce/pull/1552) [`18054c441`](https://github.com/graphcommerce-org/graphcommerce/commit/18054c441962ba750bed3acc39ab46c8d3a341ce) Thanks [@paales](https://github.com/paales)! - Updated to Next.js v12.2.2 and other packages and made compatible
41
+
42
+ * [#1552](https://github.com/graphcommerce-org/graphcommerce/pull/1552) [`c5c539c44`](https://github.com/graphcommerce-org/graphcommerce/commit/c5c539c44eeac524cd62ce649e132d2e00333794) Thanks [@paales](https://github.com/paales)! - Make sure the gallery doesn't scroll when overlays are opened
43
+
44
+ - [#1552](https://github.com/graphcommerce-org/graphcommerce/pull/1552) [`6f69bc54c`](https://github.com/graphcommerce-org/graphcommerce/commit/6f69bc54c6e0224452817c532ae58d9c332b61ea) Thanks [@paales](https://github.com/paales)! - Prevent back button scrolling when navigating between overlays
45
+
46
+ * [#1552](https://github.com/graphcommerce-org/graphcommerce/pull/1552) [`21886d6fa`](https://github.com/graphcommerce-org/graphcommerce/commit/21886d6fa64a48d9e932bfaf8d138c9b13c36e43) Thanks [@paales](https://github.com/paales)! - Fix page stacking and scroll restoration when navigating
47
+
48
+ * Updated dependencies [[`18054c441`](https://github.com/graphcommerce-org/graphcommerce/commit/18054c441962ba750bed3acc39ab46c8d3a341ce), [`21886d6fa`](https://github.com/graphcommerce-org/graphcommerce/commit/21886d6fa64a48d9e932bfaf8d138c9b13c36e43), [`b4936e961`](https://github.com/graphcommerce-org/graphcommerce/commit/b4936e96175fe80717895822e245274db05638bd)]:
49
+ - @graphcommerce/framer-next-pages@3.2.4
50
+ - @graphcommerce/framer-scroller@2.1.23
51
+
3
52
  ## 4.13.0
4
53
 
5
54
  ### Minor Changes
@@ -119,6 +119,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
119
119
  <Row maxWidth={false} disableGutters className={classes.row} sx={sx}>
120
120
  <MotionBox
121
121
  layout
122
+ layoutDependency={zoomed}
122
123
  className={classes.root}
123
124
  sx={[
124
125
  {
@@ -146,6 +147,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
146
147
  >
147
148
  <MotionBox
148
149
  layout
150
+ layoutDependency={zoomed}
149
151
  className={classes.scrollerContainer}
150
152
  sx={[
151
153
  {
@@ -193,6 +195,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
193
195
  <MotionImageAspect
194
196
  key={typeof image.src === 'string' ? image.src : idx}
195
197
  layout
198
+ layoutDependency={zoomed}
196
199
  src={image.src}
197
200
  width={image.width}
198
201
  height={image.height}
@@ -209,6 +212,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
209
212
  </Scroller>
210
213
  <MotionBox
211
214
  layout
215
+ layoutDependency={zoomed}
212
216
  className={classes.topRight}
213
217
  sx={{
214
218
  display: 'grid',
@@ -243,6 +247,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
243
247
  >
244
248
  <ScrollerButton
245
249
  layout
250
+ layoutDependency={zoomed}
246
251
  direction='left'
247
252
  size='small'
248
253
  className={classes.sliderButtons}
@@ -262,6 +267,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
262
267
  >
263
268
  <ScrollerButton
264
269
  layout
270
+ layoutDependency={zoomed}
265
271
  direction='right'
266
272
  size='small'
267
273
  className={classes.sliderButtons}
@@ -286,7 +292,11 @@ export function SidebarGallery(props: SidebarGalleryProps) {
286
292
  },
287
293
  }}
288
294
  >
289
- <ScrollerDots layout sx={{ backgroundColor: 'background.paper', boxShadow: 6 }} />
295
+ <ScrollerDots
296
+ layout
297
+ layoutDependency={zoomed}
298
+ sx={{ backgroundColor: 'background.paper', boxShadow: 6 }}
299
+ />
290
300
  </Box>
291
301
  </MotionBox>
292
302
 
@@ -319,6 +329,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
319
329
  >
320
330
  <MotionBox
321
331
  layout
332
+ layoutDependency={zoomed}
322
333
  className={classes.sidebar}
323
334
  sx={{
324
335
  boxSizing: 'border-box',
@@ -1,6 +1,6 @@
1
1
  import { useMotionValueValue } from '@graphcommerce/framer-utils'
2
2
  import { Box, styled, SxProps, Theme } from '@mui/material'
3
- import { m } from 'framer-motion'
3
+ import { LayoutProps, m } from 'framer-motion'
4
4
  import React, { useRef } from 'react'
5
5
  import { extendableComponent } from '../../Styles'
6
6
  import { useScrollY } from '../hooks/useScrollY'
@@ -16,6 +16,7 @@ export type LayoutHeaderContentProps = FloatingProps & {
16
16
  switchPoint?: number
17
17
  sx?: SxProps<Theme>
18
18
  sxBg?: SxProps<Theme>
19
+ layout?: LayoutProps['layout']
19
20
  }
20
21
 
21
22
  type OwnerState = { floatingSm: boolean; floatingMd: boolean; scrolled: boolean; divider: boolean }
@@ -36,6 +37,7 @@ export function LayoutHeaderContent(props: LayoutHeaderContentProps) {
36
37
  switchPoint = 50,
37
38
  sx = [],
38
39
  sxBg = [],
40
+ layout,
39
41
  } = props
40
42
 
41
43
  const scroll = useScrollY()
@@ -142,7 +144,7 @@ export function LayoutHeaderContent(props: LayoutHeaderContentProps) {
142
144
  justifyContent: 'start',
143
145
  })}
144
146
  >
145
- <MotionDiv layout='position'>{left}</MotionDiv>
147
+ <MotionDiv layout={layout}>{left}</MotionDiv>
146
148
  </Box>
147
149
  )}
148
150
  <Box
@@ -175,7 +177,7 @@ export function LayoutHeaderContent(props: LayoutHeaderContentProps) {
175
177
  },
176
178
  })}
177
179
  >
178
- <MotionDiv layout='position'>{children}</MotionDiv>
180
+ <MotionDiv layout={layout}>{children}</MotionDiv>
179
181
  </Box>
180
182
  <Box
181
183
  className={classes.right}
@@ -191,7 +193,7 @@ export function LayoutHeaderContent(props: LayoutHeaderContentProps) {
191
193
  justifyContent: 'end',
192
194
  })}
193
195
  >
194
- <MotionDiv layout='position'>{right}</MotionDiv>
196
+ <MotionDiv layout={layout}>{right}</MotionDiv>
195
197
  </Box>
196
198
  {divider && (
197
199
  <Box
@@ -39,8 +39,10 @@ export function LayoutDefault(props: LayoutDefaultProps) {
39
39
  sx = [],
40
40
  } = props
41
41
 
42
- const offset = useScrollOffset().y
43
- const scrollWithOffset = useTransform(useViewportScroll().scrollY, (y) => y + offset)
42
+ const scrollWithOffset = useTransform(
43
+ [useViewportScroll().scrollY, useScrollOffset()],
44
+ ([y, offset]: number[]) => y + offset,
45
+ )
44
46
 
45
47
  const classes = withState({ noSticky })
46
48
 
@@ -1,5 +1,6 @@
1
1
  import { usePageContext, useGo, useScrollOffset } from '@graphcommerce/framer-next-pages'
2
2
  import { ScrollerProvider, ScrollSnapType } from '@graphcommerce/framer-scroller'
3
+ import { useMotionValueValue } from '@graphcommerce/framer-utils'
3
4
  import { usePresence } from 'framer-motion'
4
5
  import type { SetOptional } from 'type-fest'
5
6
  import { OverlayBase, LayoutOverlayBaseProps } from '../../Overlay/components/OverlayBase'
@@ -19,7 +20,7 @@ export function LayoutOverlay(props: LayoutOverlayProps) {
19
20
 
20
21
  const { closeSteps, active, direction } = usePageContext()
21
22
  const onCloseHandler = useGo(closeSteps * -1)
22
- const offsetPageY = useScrollOffset().y
23
+ const offsetPageY = useMotionValueValue(useScrollOffset(), (v) => v)
23
24
  const [isPresent, safeToRemove] = usePresence()
24
25
 
25
26
  return (
@@ -3,7 +3,7 @@ import { LayoutOverlay, LayoutOverlayProps } from '../components/LayoutOverlay'
3
3
 
4
4
  export type LayoutOverlayState = Omit<
5
5
  LayoutOverlayProps,
6
- 'children' | 'sx' | 'sxBackdrop' | 'mdSpacingTop' | 'smSpacingTop'
6
+ 'children' | 'sx' | 'sxBackdrop' | 'mdSpacingTop' | 'smSpacingTop' | 'overlayPaneProps'
7
7
  >
8
8
 
9
9
  export function useLayoutState() {
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-use-before-define */
2
- import { Box, ListItemButton, styled, useEventCallback } from '@mui/material'
2
+ import { Box, ListItemButton, styled, Theme, useEventCallback, useMediaQuery } from '@mui/material'
3
3
  import PageLink from 'next/link'
4
+ import { useEffect } from 'react'
4
5
  import { IconSvg } from '../../IconSvg'
5
6
  import { extendableComponent } from '../../Styles/extendableComponent'
6
7
  import { iconChevronRight } from '../../icons'
@@ -26,7 +27,12 @@ type NavigationItemProps = NavigationNode & {
26
27
  parentPath: NavigationPath
27
28
  idx: number
28
29
  NavigationList: typeof NavigationList
29
- } & OwnerState
30
+ } & OwnerState &
31
+ mouseEventPref
32
+
33
+ export type mouseEventPref = {
34
+ mouseEvent: 'click' | 'hover'
35
+ }
30
36
 
31
37
  const componentName = 'NavigationItem'
32
38
  const parts = ['li', 'ul', 'item'] as const
@@ -39,10 +45,10 @@ const { withState } = extendableComponent<OwnerState, typeof componentName, type
39
45
  const NavigationLI = styled('li')({ display: 'contents' })
40
46
 
41
47
  export function NavigationItem(props: NavigationItemProps) {
42
- const { id, parentPath, idx, first, last, NavigationList } = props
48
+ const { id, parentPath, idx, first, last, NavigationList, mouseEvent } = props
43
49
 
44
50
  const row = idx + 1
45
- const { selected, select, hideRootOnNavigate, onClose } = useNavigation()
51
+ const { selected, select, hideRootOnNavigate, onClose, animating } = useNavigation()
46
52
 
47
53
  const itemPath = [...parentPath, id]
48
54
  const isSelected = selected.slice(0, itemPath.length).join('/') === itemPath.join('/')
@@ -59,6 +65,8 @@ export function NavigationItem(props: NavigationItemProps) {
59
65
  onClose?.(e, href)
60
66
  })
61
67
 
68
+ const isDesktop = useMediaQuery<Theme>((theme) => theme.breakpoints.up('md'))
69
+
62
70
  if (isNavigationButton(props)) {
63
71
  const { childItems, name } = props
64
72
  return (
@@ -66,18 +74,44 @@ export function NavigationItem(props: NavigationItemProps) {
66
74
  <ListItemButton
67
75
  className={classes.item}
68
76
  role='button'
69
- sx={{
70
- gridRowStart: row,
71
- gridColumnStart: column,
72
- gap: (theme) => theme.spacings.xxs,
73
- display: hideItem ? 'none' : 'flex',
74
- }}
77
+ sx={[
78
+ (theme) => ({
79
+ gridRowStart: row,
80
+ gridColumnStart: column,
81
+ gap: theme.spacings.xxs,
82
+ display: hideItem ? 'none' : 'flex',
83
+ '&.Mui-disabled': {
84
+ opacity: 1,
85
+ background: theme.palette.action.hover,
86
+ },
87
+ }),
88
+ mouseEvent === 'hover'
89
+ ? {
90
+ '&.Mui-disabled': {
91
+ cursor: 'pointer',
92
+ pointerEvents: 'auto',
93
+ },
94
+ }
95
+ : {},
96
+ ]}
75
97
  disabled={isSelected}
76
98
  tabIndex={selected.join(',').includes(parentPath.join(',')) ? undefined : -1}
77
99
  onClick={(e) => {
78
100
  e.preventDefault()
79
- if (!isSelected) select(itemPath)
101
+ if (!isSelected && animating.current === false) {
102
+ select(itemPath)
103
+ }
80
104
  }}
105
+ onMouseEnter={
106
+ itemPath.length > 1 && mouseEvent === 'hover'
107
+ ? (e) => {
108
+ if (isDesktop && animating.current === false && !isSelected) {
109
+ e.preventDefault()
110
+ setTimeout(() => select(itemPath), 0)
111
+ }
112
+ }
113
+ : undefined
114
+ }
81
115
  >
82
116
  <Box
83
117
  component='span'
@@ -93,7 +127,12 @@ export function NavigationItem(props: NavigationItemProps) {
93
127
  <IconSvg src={iconChevronRight} sx={{ flexShrink: 0 }} />
94
128
  </ListItemButton>
95
129
 
96
- <NavigationList items={childItems} selected={isSelected} parentPath={itemPath} />
130
+ <NavigationList
131
+ items={childItems}
132
+ selected={isSelected}
133
+ parentPath={itemPath}
134
+ mouseEvent={mouseEvent}
135
+ />
97
136
  </NavigationLI>
98
137
  )
99
138
  }
@@ -1,7 +1,7 @@
1
1
  import { styled } from '@mui/material'
2
2
  import { extendableComponent } from '../../Styles/extendableComponent'
3
3
  import { NavigationNode, NavigationPath } from '../hooks/useNavigation'
4
- import { NavigationItem } from './NavigationItem'
4
+ import { NavigationItem, mouseEventPref } from './NavigationItem'
5
5
 
6
6
  const NavigationUList = styled('ul')({})
7
7
 
@@ -9,7 +9,7 @@ type NavigationItemsProps = {
9
9
  parentPath?: NavigationPath
10
10
  items: NavigationNode[]
11
11
  selected?: boolean
12
- }
12
+ } & mouseEventPref
13
13
 
14
14
  type OwnerState = {
15
15
  column: number
@@ -23,7 +23,7 @@ const { withState } = extendableComponent<OwnerState, typeof name, typeof parts>
23
23
  // const parts = ['li', 'ul', 'item'] as const
24
24
 
25
25
  export function NavigationList(props: NavigationItemsProps) {
26
- const { items, parentPath = [], selected = false } = props
26
+ const { items, parentPath = [], selected = false, mouseEvent } = props
27
27
 
28
28
  return (
29
29
  <NavigationUList
@@ -43,6 +43,7 @@ export function NavigationList(props: NavigationItemsProps) {
43
43
  first={idx === 0}
44
44
  last={idx === items.length - 1}
45
45
  column={0}
46
+ mouseEvent={mouseEvent}
46
47
  />
47
48
  ))}
48
49
  </NavigationUList>
@@ -3,6 +3,7 @@ import { i18n } from '@lingui/core'
3
3
  import { Trans } from '@lingui/react'
4
4
  import { Box, Fab, SxProps, Theme, useEventCallback, useMediaQuery } from '@mui/material'
5
5
  import { m } from 'framer-motion'
6
+ import { useState } from 'react'
6
7
  import { IconSvg, useIconSvgSize } from '../../IconSvg'
7
8
  import { LayoutHeaderContent } from '../../Layout/components/LayoutHeaderContent'
8
9
  import { LayoutTitle } from '../../Layout/components/LayoutTitle'
@@ -18,14 +19,26 @@ import {
18
19
  NavigationNodeHref,
19
20
  useNavigation,
20
21
  } from '../hooks/useNavigation'
22
+ import { mouseEventPref } from './NavigationItem'
21
23
  import { NavigationList } from './NavigationList'
22
24
 
25
+ type LayoutOverlayVariant = 'left' | 'bottom' | 'right'
26
+ type LayoutOverlaySize = 'floating' | 'minimal' | 'full'
27
+ type LayoutOverlayAlign = 'start' | 'end' | 'center' | 'stretch'
28
+
23
29
  type NavigationOverlayProps = {
24
30
  active: boolean
25
31
  sx?: SxProps<Theme>
26
32
  stretchColumns?: boolean
27
- itemWidth: string
28
- }
33
+ variantSm: LayoutOverlayVariant
34
+ variantMd: LayoutOverlayVariant
35
+ sizeSm?: LayoutOverlaySize
36
+ sizeMd?: LayoutOverlaySize
37
+ justifySm?: LayoutOverlayAlign
38
+ justifyMd?: LayoutOverlayAlign
39
+ itemWidthSm?: string
40
+ itemWidthMd?: string
41
+ } & mouseEventPref
29
42
 
30
43
  function findCurrent(
31
44
  items: NavigationContextType['items'],
@@ -55,8 +68,21 @@ const parts = ['root', 'navigation', 'header', 'column'] as const
55
68
  const { classes } = extendableComponent(componentName, parts)
56
69
 
57
70
  export function NavigationOverlay(props: NavigationOverlayProps) {
58
- const { active, sx, stretchColumns, itemWidth } = props
59
- const { selected, select, items, onClose } = useNavigation()
71
+ const {
72
+ active,
73
+ sx,
74
+ stretchColumns,
75
+ variantMd,
76
+ variantSm,
77
+ justifyMd,
78
+ justifySm,
79
+ sizeMd,
80
+ sizeSm,
81
+ itemWidthSm,
82
+ itemWidthMd,
83
+ mouseEvent,
84
+ } = props
85
+ const { selected, select, items, onClose, animating } = useNavigation()
60
86
 
61
87
  const fabSize = useFabSize('responsive')
62
88
  const svgSize = useIconSvgSize('large')
@@ -74,12 +100,20 @@ export function NavigationOverlay(props: NavigationOverlayProps) {
74
100
  className={classes.root}
75
101
  active={active}
76
102
  onClosed={onClose}
77
- variantSm='left'
78
- sizeSm='floating'
79
- justifySm='start'
80
- variantMd='left'
81
- sizeMd='floating'
82
- justifyMd='start'
103
+ variantSm={variantSm}
104
+ sizeSm={sizeSm}
105
+ justifySm={justifySm}
106
+ variantMd={variantMd}
107
+ sizeMd={sizeMd}
108
+ justifyMd={justifyMd}
109
+ overlayPaneProps={{
110
+ onLayoutAnimationStart: () => {
111
+ animating.current = true
112
+ },
113
+ onLayoutAnimationComplete: () => {
114
+ animating.current = false
115
+ },
116
+ }}
83
117
  sx={{
84
118
  zIndex: 'drawer',
85
119
  '& .LayoutOverlayBase-overlayPane': {
@@ -105,6 +139,7 @@ export function NavigationOverlay(props: NavigationOverlayProps) {
105
139
  floatingMd={false}
106
140
  floatingSm={false}
107
141
  switchPoint={0}
142
+ layout='position'
108
143
  left={
109
144
  showBack && (
110
145
  <Fab
@@ -150,14 +185,35 @@ export function NavigationOverlay(props: NavigationOverlayProps) {
150
185
  sx={(theme) => ({
151
186
  display: 'grid',
152
187
  alignItems: !stretchColumns ? 'start' : undefined,
153
-
188
+ '& .NavigationItem-item': {
189
+ // eslint-disable-next-line no-nested-ternary
190
+ width: itemWidthMd
191
+ ? selected.length >= 1
192
+ ? `calc(${itemWidthMd} + 1px)`
193
+ : itemWidthMd
194
+ : 'auto',
195
+ },
154
196
  [theme.breakpoints.down('md')]: {
197
+ width:
198
+ sizeSm !== 'floating'
199
+ ? `calc(${itemWidthSm || '100vw'} + ${selected.length}px)`
200
+ : `calc(${itemWidthSm || '100vw'} - ${theme.page.horizontal} - ${
201
+ theme.page.horizontal
202
+ })`,
203
+ minWidth: 200,
155
204
  overflow: 'hidden',
156
205
  scrollSnapType: 'x mandatory',
157
- width: `calc(${theme.spacings.md} + ${theme.spacings.md} + ${itemWidth})`,
158
- },
159
- '& .NavigationItem-item': {
160
- width: itemWidth,
206
+ '& .NavigationItem-item': {
207
+ width:
208
+ sizeSm !== 'floating'
209
+ ? `calc(${itemWidthSm || '100vw'} - ${theme.spacings.md} - ${
210
+ theme.spacings.md
211
+ } + ${selected.length}px)`
212
+ : `calc(${itemWidthSm || '100vw'} - ${theme.spacings.md} - ${
213
+ theme.spacings.md
214
+ } - ${theme.page.horizontal} - ${theme.page.horizontal})`,
215
+ minWidth: `calc(${200}px - ${theme.spacings.md} - ${theme.spacings.md})`,
216
+ },
161
217
  },
162
218
  })}
163
219
  >
@@ -227,7 +283,7 @@ export function NavigationOverlay(props: NavigationOverlayProps) {
227
283
  />
228
284
  )}
229
285
 
230
- <NavigationList items={items} selected />
286
+ <NavigationList items={items} selected mouseEvent={mouseEvent} />
231
287
  </Box>
232
288
  </Box>
233
289
  </MotionDiv>
@@ -1,6 +1,6 @@
1
1
  import { useEventCallback } from '@mui/material'
2
2
  import { MotionConfig } from 'framer-motion'
3
- import { useState, useMemo } from 'react'
3
+ import { useState, useMemo, SetStateAction, useRef } from 'react'
4
4
  import { isElement } from 'react-is'
5
5
  import {
6
6
  NavigationNode,
@@ -16,6 +16,8 @@ export type NavigationProviderProps = {
16
16
  closeAfterNavigate?: boolean
17
17
  children?: React.ReactNode
18
18
  animationDuration?: number
19
+ selected: NavigationPath
20
+ setSelected: (value: SetStateAction<NavigationPath>) => void
19
21
  onChange?: NavigationSelect
20
22
  onClose?: NavigationContextType['onClose']
21
23
  }
@@ -31,9 +33,11 @@ export function NavigationProvider(props: NavigationProviderProps) {
31
33
  animationDuration = 0.275,
32
34
  children,
33
35
  onClose: onCloseUnstable,
36
+ selected,
37
+ setSelected,
34
38
  } = props
35
39
 
36
- const [selected, setSelected] = useState<NavigationPath>([])
40
+ const animating = useRef(false)
37
41
 
38
42
  const select = useEventCallback((incomming: NavigationPath) => {
39
43
  setSelected(incomming)
@@ -50,6 +54,7 @@ export function NavigationProvider(props: NavigationProviderProps) {
50
54
  hideRootOnNavigate,
51
55
  selected,
52
56
  select,
57
+ animating,
53
58
  items: items
54
59
  .map((item, index) => (isElement(item) ? { id: item.key ?? index, component: item } : item))
55
60
  .filter(nonNullable),
@@ -1,4 +1,4 @@
1
- import { createContext, useContext } from 'react'
1
+ import { createContext, MutableRefObject, SetStateAction, useContext } from 'react'
2
2
 
3
3
  export type NavigationId = string | number
4
4
  export type NavigationPath = NavigationId[]
@@ -17,6 +17,7 @@ export type NavigationContextType = {
17
17
  items: NavigationNode[]
18
18
  hideRootOnNavigate: boolean
19
19
  onClose: NavigationOnClose
20
+ animating: MutableRefObject<boolean>
20
21
  }
21
22
 
22
23
  type NavigationNodeBase = {
@@ -5,7 +5,7 @@ import {
5
5
  useIsomorphicLayoutEffect,
6
6
  } from '@graphcommerce/framer-utils'
7
7
  import { Box, styled, SxProps, Theme, useTheme, useThemeProps } from '@mui/material'
8
- import { m, useDomEvent, useMotionValue, useTransform } from 'framer-motion'
8
+ import { m, MotionProps, useDomEvent, useMotionValue, useTransform } from 'framer-motion'
9
9
  import React, { useCallback, useEffect, useRef } from 'react'
10
10
  import { LayoutProvider } from '../../Layout/components/LayoutProvider'
11
11
  import { ExtendableComponent, extendableComponent } from '../../Styles'
@@ -40,6 +40,7 @@ export type LayoutOverlayBaseProps = {
40
40
  offsetPageY: number
41
41
  isPresent: boolean
42
42
  safeToRemove: (() => void) | null | undefined
43
+ overlayPaneProps?: MotionProps
43
44
  } & StyleProps &
44
45
  OverridableProps
45
46
 
@@ -86,6 +87,7 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
86
87
  offsetPageY,
87
88
  isPresent,
88
89
  safeToRemove,
90
+ overlayPaneProps,
89
91
  } = props
90
92
 
91
93
  const th = useTheme()
@@ -367,6 +369,7 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
367
369
  })}
368
370
  >
369
371
  <MotionDiv
372
+ {...overlayPaneProps}
370
373
  layout
371
374
  className={classes.overlayPane}
372
375
  sx={(theme) => ({
@@ -1,10 +1,8 @@
1
1
  import { usePageContext } from '@graphcommerce/framer-next-pages'
2
- import {
3
- resolveHref,
4
- addBasePath,
5
- addLocale,
6
- getDomainLocale,
7
- } from 'next/dist/shared/lib/router/router'
2
+ import { addBasePath } from 'next/dist/client/add-base-path'
3
+ import { addLocale } from 'next/dist/client/add-locale'
4
+ import { getDomainLocale } from 'next/dist/client/get-domain-locale'
5
+ import { resolveHref } from 'next/dist/shared/lib/router/router'
8
6
  import Head from 'next/head'
9
7
  import { useRouter } from 'next/router'
10
8
 
package/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './ActionCard/ActionCard'
2
2
  export * from './ActionCard/ActionCardList'
3
+ export * from './ActionCard/ActionCardListForm'
3
4
  export * from './AnimatedRow/AnimatedRow'
4
5
  export * from './Blog/BlogAuthor/BlogAuthor'
5
6
  export * from './Blog/BlogContent/BlogContent'
@@ -55,3 +56,4 @@ export * from './ToggleButton/ToggleButton'
55
56
  export * from './ToggleButtonGroup/ToggleButtonGroup'
56
57
  export * from './UspList/UspList'
57
58
  export * from './UspList/UspListItem'
59
+ export * from './utils/cookie'
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.13.0",
5
+ "version": "4.15.0",
6
6
  "author": "",
7
7
  "license": "MIT",
8
8
  "sideEffects": false,
@@ -15,15 +15,16 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@emotion/babel-preset-css-prop": "^11.2.0",
18
- "@emotion/cache": "^11.7.1",
19
- "@emotion/react": "^11.9.0",
18
+ "@emotion/cache": "^11.9.3",
19
+ "@emotion/react": "^11.9.3",
20
20
  "@emotion/server": "^11.4.0",
21
- "@emotion/styled": "^11.6.0",
22
- "@graphcommerce/framer-next-pages": "3.2.3",
23
- "@graphcommerce/framer-scroller": "2.1.22",
21
+ "@emotion/styled": "^11.9.3",
22
+ "@graphcommerce/framer-next-pages": "3.2.4",
23
+ "@graphcommerce/framer-scroller": "2.1.25",
24
24
  "@graphcommerce/framer-utils": "3.1.4",
25
25
  "@graphcommerce/image": "3.1.7",
26
- "react-is": "^18.1.0",
26
+ "cookie": "^0.5.0",
27
+ "react-is": "^18.2.0",
27
28
  "react-schemaorg": "^2.0.0",
28
29
  "schema-dts": "^1.1.0"
29
30
  },
@@ -38,12 +39,13 @@
38
39
  "react-dom": "^18.0.0"
39
40
  },
40
41
  "devDependencies": {
41
- "@graphcommerce/eslint-config-pwa": "^4.1.8",
42
+ "@graphcommerce/eslint-config-pwa": "^4.1.9",
42
43
  "@graphcommerce/prettier-config-pwa": "^4.0.6",
43
- "@graphcommerce/typescript-config-pwa": "^4.0.3",
44
+ "@graphcommerce/typescript-config-pwa": "^4.0.4",
44
45
  "@playwright/test": "^1.21.1",
46
+ "@types/cookie": "^0.5.1",
45
47
  "@types/react-is": "^17.0.3",
46
48
  "type-fest": "^2.12.2",
47
- "typescript": "4.7.3"
49
+ "typescript": "4.7.4"
48
50
  }
49
51
  }
@@ -0,0 +1,33 @@
1
+ import { serialize, parse, CookieSerializeOptions } from 'cookie'
2
+
3
+ /** Read a cookie */
4
+ export function cookie(name: string): string | undefined
5
+ /** Set a cookie */
6
+ export function cookie(name: string, value: string, options?: CookieSerializeOptions): void
7
+ /** Delete a cookie */
8
+ export function cookie(name: string, value: null): void
9
+ /** Function to handle the three different cases */
10
+ export function cookie(name: string, value?: string | null, options?: CookieSerializeOptions) {
11
+ if (typeof window === 'undefined') {
12
+ return undefined
13
+ }
14
+
15
+ // Read a cookie
16
+ if (typeof value === 'undefined') return parse(document.cookie)[name]
17
+
18
+ // Set a cookie
19
+ if (typeof value === 'string') {
20
+ const serialized = serialize(name, value, { path: '/', maxAge: 31536000, ...options })
21
+ document.cookie = serialized
22
+ return undefined
23
+ }
24
+
25
+ // Delete a cookie
26
+ if (value === null) {
27
+ const serialized = serialize(name, '', { path: '/', maxAge: 0 })
28
+ document.cookie = serialized
29
+ return undefined
30
+ }
31
+
32
+ return undefined
33
+ }