@graphcommerce/next-ui 4.27.0 → 4.28.1

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.
@@ -12,7 +12,7 @@ function isButtonProps(props: ButtonProps<'div'> | BoxProps<'div'>): props is Bu
12
12
  return props.onClick !== undefined
13
13
  }
14
14
 
15
- const RenderComponent = (props: ButtonProps<'div'> | BoxProps<'div'>) =>
15
+ const ButtonOrBox = (props: ButtonProps<'div'> | BoxProps<'div'>) =>
16
16
  isButtonProps(props) ? <ButtonBase component='div' {...props} /> : <Box {...props} />
17
17
 
18
18
  export type ActionCardProps = {
@@ -101,26 +101,26 @@ export function ActionCard(props: ActionCardProps) {
101
101
  })
102
102
 
103
103
  return (
104
- <RenderComponent
104
+ <ButtonOrBox
105
105
  className={classes.root}
106
106
  onClick={onClick && ((event) => onClick?.(event, value))}
107
107
  disabled={disabled}
108
108
  sx={[
109
109
  (theme) => ({
110
110
  '&.sizeSmall': {
111
- padding: `5px 10px`,
111
+ py: `5px`,
112
112
  display: 'flex',
113
113
  typography: 'body2',
114
114
  },
115
115
 
116
116
  '&.sizeMedium': {
117
- padding: `10px 12px`,
117
+ py: `10px`,
118
118
  typography: 'body2',
119
119
  display: 'block',
120
120
  },
121
121
 
122
122
  '&.sizeLarge': {
123
- padding: `${theme.spacings.xxs} ${theme.spacings.xs}`,
123
+ py: theme.spacings.xxs,
124
124
  display: 'block',
125
125
  },
126
126
 
@@ -184,6 +184,10 @@ export function ActionCard(props: ActionCardProps) {
184
184
  ),
185
185
  },
186
186
 
187
+ '&.sizeSmall': { px: `10px` },
188
+ '&.sizeMedium': { px: `12px` },
189
+ '&.sizeLarge': { px: theme.spacings.xs },
190
+
187
191
  '&.selected': {
188
192
  border: `2px solid ${theme.palette[color].main}`,
189
193
  boxShadow: `0 0 0 4px ${alpha(
@@ -223,6 +227,7 @@ export function ActionCard(props: ActionCardProps) {
223
227
  flexDirection: 'row',
224
228
  width: '100%',
225
229
  justifyContent: 'space-between',
230
+ alignContent: 'stretch',
226
231
  }}
227
232
  >
228
233
  <Box
@@ -230,6 +235,7 @@ export function ActionCard(props: ActionCardProps) {
230
235
  display: 'flex',
231
236
  flexDirection: 'row',
232
237
  justifyContent: 'space-between',
238
+ alignContent: 'stretch',
233
239
  }}
234
240
  >
235
241
  {image && (
@@ -302,6 +308,6 @@ export function ActionCard(props: ActionCardProps) {
302
308
  </Box>
303
309
  </Box>
304
310
  {after && <Box className={classes.after}>{after}</Box>}
305
- </RenderComponent>
311
+ </ButtonOrBox>
306
312
  )
307
313
  }
@@ -0,0 +1,57 @@
1
+ import { Box, BoxProps } from '@mui/material'
2
+ import React from 'react'
3
+ import { extendableComponent } from '../Styles'
4
+ import { ActionCardProps } from './ActionCard'
5
+
6
+ type ActionCardLayoutProps = {
7
+ children?: React.ReactNode
8
+ } & Pick<ActionCardProps, 'layout'> &
9
+ BoxProps
10
+
11
+ const parts = ['root'] as const
12
+ const name = 'ActionCardLayout'
13
+ const { withState } = extendableComponent<
14
+ Pick<ActionCardProps, 'layout'>,
15
+ typeof name,
16
+ typeof parts
17
+ >(name, parts)
18
+
19
+ export const ActionCardLayout = React.forwardRef<HTMLDivElement, ActionCardLayoutProps>(
20
+ (props, ref) => {
21
+ const { layout = 'list' } = props
22
+
23
+ const classes = withState({ layout })
24
+
25
+ return (
26
+ <Box
27
+ ref={ref}
28
+ {...props}
29
+ className={classes.root}
30
+ sx={[
31
+ (theme) => ({
32
+ '&.layoutStack': {
33
+ display: 'grid',
34
+ height: 'min-content',
35
+ gap: theme.spacings.xxs,
36
+ },
37
+ '&.layoutList': {
38
+ display: 'grid',
39
+ height: 'min-content',
40
+ },
41
+ '&.layoutGrid': {
42
+ display: 'grid',
43
+ gridTemplateColumns: 'repeat(2, 1fr)',
44
+ gap: theme.spacings.xxs,
45
+ },
46
+ '&.layoutInline': {
47
+ display: 'flex',
48
+ flexWrap: 'wrap',
49
+ gap: theme.spacings.xxs,
50
+ },
51
+ }),
52
+ ...(Array.isArray(props.sx) ? props.sx : [props.sx]),
53
+ ]}
54
+ />
55
+ )
56
+ },
57
+ )
@@ -3,6 +3,7 @@ import React from 'react'
3
3
  import { isFragment } from 'react-is'
4
4
  import { extendableComponent } from '../Styles'
5
5
  import { ActionCardProps } from './ActionCard'
6
+ import { ActionCardLayout } from './ActionCardLayout'
6
7
 
7
8
  type MultiSelect = {
8
9
  multiple: true
@@ -132,52 +133,25 @@ export const ActionCardList = React.forwardRef<HTMLDivElement, ActionCardListPro
132
133
 
133
134
  return (
134
135
  <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
- >
136
+ <ActionCardLayout sx={sx} className={classes.root} layout={layout}>
164
137
  {childReactNodes.map((child) => {
165
138
  if (collapse && Boolean(value) && !isValueSelected(child.props.value, value))
166
139
  return null
167
140
  return React.cloneElement(child, {
168
141
  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,
142
+ error,
143
+ color,
144
+ variant,
145
+ size,
146
+ layout,
147
+ ...child.props,
174
148
  selected:
175
149
  child.props.selected === undefined
176
150
  ? isValueSelected(child.props.value, value)
177
151
  : child.props.selected,
178
152
  })
179
153
  })}
180
- </Box>
154
+ </ActionCardLayout>
181
155
  {error && errorMessage && (
182
156
  <Alert
183
157
  severity='error'
@@ -0,0 +1,4 @@
1
+ export * from './ActionCard'
2
+ export * from './ActionCardLayout'
3
+ export * from './ActionCardList'
4
+ export * from './ActionCardListForm'
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.28.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1675](https://github.com/graphcommerce-org/graphcommerce/pull/1675) [`9e630670f`](https://github.com/graphcommerce-org/graphcommerce/commit/9e630670ff6c952ab7b938d890b5509804985cf3) Thanks [@paales](https://github.com/paales)! - Added a new ItemScroller component to be able to make horizontal product scrollers
8
+
9
+ - [#1675](https://github.com/graphcommerce-org/graphcommerce/pull/1675) [`2e9fa5984`](https://github.com/graphcommerce-org/graphcommerce/commit/2e9fa5984a07ff14fc1b3a4f62189a26e8e3ecdd) Thanks [@paales](https://github.com/paales)! - Measure the size of children of the overlay to determine the size of children
10
+
11
+ - [#1675](https://github.com/graphcommerce-org/graphcommerce/pull/1675) [`adf13069a`](https://github.com/graphcommerce-org/graphcommerce/commit/adf13069af6460c960276b402237371c12fc6dec) Thanks [@paales](https://github.com/paales)! - Use realtime measurements for useOverlayPosition instead of computed values, to improve flickering issues
12
+
13
+ - [#1675](https://github.com/graphcommerce-org/graphcommerce/pull/1675) [`1b1504c9b`](https://github.com/graphcommerce-org/graphcommerce/commit/1b1504c9b0e51f2787bce91e1ff1940f540411d6) Thanks [@paales](https://github.com/paales)! - Added crosssel functionality
14
+
15
+ - Updated dependencies [[`81f31d1e5`](https://github.com/graphcommerce-org/graphcommerce/commit/81f31d1e54397368088a4289aaddd29facfceeef), [`a8905d263`](https://github.com/graphcommerce-org/graphcommerce/commit/a8905d263273cb9322583d5759a5fdc66eceb8e4), [`1b1504c9b`](https://github.com/graphcommerce-org/graphcommerce/commit/1b1504c9b0e51f2787bce91e1ff1940f540411d6), [`6c2e27b1b`](https://github.com/graphcommerce-org/graphcommerce/commit/6c2e27b1be4aaa888e65a2bd69eaeb467a54a023)]:
16
+ - @graphcommerce/framer-scroller@2.1.41
17
+ - @graphcommerce/framer-utils@3.2.1
18
+ - @graphcommerce/framer-next-pages@3.3.2
19
+ - @graphcommerce/image@3.1.10
20
+
21
+ ## 4.28.0
22
+
23
+ ### Minor Changes
24
+
25
+ - [#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
26
+
27
+ * [#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
28
+
29
+ ### Patch Changes
30
+
31
+ - [#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
32
+
33
+ - Updated dependencies [[`f5eae0afd`](https://github.com/graphcommerce-org/graphcommerce/commit/f5eae0afdbd474b1f81c450425ffadf2d025187a), [`9e0ca73eb`](https://github.com/graphcommerce-org/graphcommerce/commit/9e0ca73eb50ded578f4a98e40a7eb920bf8ab421)]:
34
+ - @graphcommerce/framer-scroller@2.1.40
35
+ - @graphcommerce/framer-next-pages@3.3.1
36
+
3
37
  ## 4.27.0
4
38
 
5
39
  ### Minor Changes
@@ -0,0 +1,95 @@
1
+ import {
2
+ ScrollerProvider,
3
+ Scroller,
4
+ ScrollerButton,
5
+ ScrollerButtonProps,
6
+ } from '@graphcommerce/framer-scroller'
7
+ import { Box, SxProps, Theme } from '@mui/material'
8
+ import { IconSvg } from '../IconSvg'
9
+ import { extendableComponent, responsiveVal } from '../Styles'
10
+ import { useFabSize } from '../Theme'
11
+ import { iconChevronLeft, iconChevronRight } from '../icons'
12
+
13
+ const { classes } = extendableComponent('SidebarSlider', [
14
+ 'root',
15
+ 'grid',
16
+ 'sidebar',
17
+ 'scrollerContainer',
18
+ 'scroller',
19
+ 'sliderButtons',
20
+ 'centerLeft',
21
+ 'centerRight',
22
+ ] as const)
23
+
24
+ type SliderProps = {
25
+ children: React.ReactNode
26
+ sx?: SxProps<Theme>
27
+ buttonSize?: ScrollerButtonProps['size']
28
+ }
29
+
30
+ export function ItemScroller(props: SliderProps) {
31
+ const { children, sx, buttonSize = 'responsive' } = props
32
+
33
+ const size = useFabSize(buttonSize)
34
+
35
+ return (
36
+ <Box sx={sx} className={classes.root}>
37
+ <Box sx={{ position: 'relative', minWidth: 1 }} className={classes.scrollerContainer}>
38
+ <ScrollerProvider scrollSnapAlign='start'>
39
+ <Scroller
40
+ className={classes.scroller}
41
+ hideScrollbar
42
+ sx={(theme) => ({
43
+ gridColumnGap: theme.spacings.md,
44
+ gridRowGap: theme.spacings.lg,
45
+ // paddingRight: theme.page.horizontal,
46
+ px: theme.page.horizontal,
47
+ scrollPaddingLeft: theme.page.horizontal,
48
+ scrollPaddingRight: theme.page.horizontal,
49
+ gridAutoColumns: responsiveVal(200, 300),
50
+ })}
51
+ >
52
+ {children}
53
+ </Scroller>
54
+ <Box
55
+ className={classes.centerLeft}
56
+ sx={(theme) => ({
57
+ display: 'grid',
58
+ gridAutoFlow: 'row',
59
+ gap: theme.spacings.xxs,
60
+ position: 'absolute',
61
+ left: theme.spacings.sm,
62
+ top: `calc(50% - 28px)`,
63
+ })}
64
+ >
65
+ <ScrollerButton
66
+ direction='left'
67
+ sx={{ display: { xs: 'none', md: 'flex' } }}
68
+ size='responsive'
69
+ >
70
+ <IconSvg src={iconChevronLeft} />
71
+ </ScrollerButton>
72
+ </Box>
73
+ <Box
74
+ className={classes.centerRight}
75
+ sx={(theme) => ({
76
+ display: 'grid',
77
+ gap: theme.spacings.xxs,
78
+ position: 'absolute',
79
+ right: theme.spacings.sm,
80
+ top: `calc(50% - (${size}/2))`,
81
+ })}
82
+ >
83
+ <ScrollerButton
84
+ direction='right'
85
+ sx={{ display: { xs: 'none', md: 'flex' } }}
86
+ size='responsive'
87
+ >
88
+ <IconSvg src={iconChevronRight} />
89
+ </ScrollerButton>
90
+ </Box>
91
+ </ScrollerProvider>
92
+ </Box>
93
+ </Box>
94
+ )
95
+ }
@@ -10,7 +10,7 @@ import {
10
10
  import { Fab, useTheme, Box, styled, SxProps, Theme } from '@mui/material'
11
11
  import { m, useDomEvent, useMotionValue } from 'framer-motion'
12
12
  import { useRouter } from 'next/router'
13
- import React, { useEffect, useMemo, useRef, useState } from 'react'
13
+ import React, { useEffect, useRef } from 'react'
14
14
  import { IconSvg } from '../IconSvg'
15
15
  import { Row } from '../Row/Row'
16
16
  import { extendableComponent } from '../Styles'
@@ -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,7 +1,7 @@
1
1
  import {
2
2
  Scroller,
3
3
  ScrollerButton,
4
- ScrollerButtonProps as ScrollerButtonPropsType,
4
+ ScrollerButtonProps,
5
5
  ScrollerPageCounter,
6
6
  ScrollerProvider,
7
7
  } from '@graphcommerce/framer-scroller'
@@ -28,7 +28,7 @@ export type SidebarSliderProps = {
28
28
  children: ReactNode
29
29
  sidebar: ReactNode
30
30
  sx?: SxProps<Theme>
31
- buttonSize?: ScrollerButtonPropsType['size']
31
+ buttonSize?: ScrollerButtonProps['size']
32
32
  }
33
33
 
34
34
  export function SidebarSlider(props: SidebarSliderProps) {
@@ -1,2 +1,3 @@
1
1
  export * from './SidebarGallery'
2
2
  export * from './SidebarSlider'
3
+ export * from './ItemScroller'
@@ -158,6 +158,7 @@ export function LayoutHeaderContent(props: LayoutHeaderContentProps) {
158
158
  gridArea: 'center',
159
159
  justifyContent: 'start',
160
160
  overflow: 'hidden',
161
+ justifySelf: 'center',
161
162
 
162
163
  transition: `opacity 150ms`,
163
164
  opacity: 0,
@@ -25,7 +25,11 @@ export function LayoutOverlay(props: LayoutOverlayProps) {
25
25
  const [isPresent, safeToRemove] = usePresence()
26
26
 
27
27
  return (
28
- <ScrollerProvider scrollSnapTypeSm={scrollSnapTypeSm} scrollSnapTypeMd={scrollSnapTypeMd}>
28
+ <ScrollerProvider
29
+ scrollSnapTypeSm={scrollSnapTypeSm}
30
+ scrollSnapTypeMd={scrollSnapTypeMd}
31
+ _inititalSnap={false}
32
+ >
29
33
  <OverlayBase
30
34
  active={active}
31
35
  direction={direction}
@@ -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}
@@ -112,7 +116,7 @@ export const NavigationOverlay = React.memo<NavigationOverlayProps>((props) => {
112
116
  sx={{
113
117
  zIndex: 'drawer',
114
118
  '& .LayoutOverlayBase-overlayPane': {
115
- minWidth: 'auto !important',
119
+ minWidth: itemWidthMd,
116
120
  width: 'max-content',
117
121
  overflow: 'hidden',
118
122
  display: 'grid',
@@ -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)
@@ -99,7 +99,7 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
99
99
  )(th)
100
100
 
101
101
  const { scrollerRef, snap } = useScrollerContext()
102
- const positions = useOverlayPosition()
102
+ const positions = useOverlayPosition(variantSm, variantMd)
103
103
  const scrollTo = useScrollTo()
104
104
  const beforeRef = useRef<HTMLDivElement>(null)
105
105
 
@@ -148,7 +148,6 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
148
148
  scroller.scrollLeft = positions.open.x.get()
149
149
  scroller.scrollTop = positions.open.y.get()
150
150
  }
151
-
152
151
  if (positions.open.visible.get() === 0) {
153
152
  scroller.scrollLeft = positions.closed.x.get()
154
153
  scroller.scrollTop = positions.closed.y.get()
@@ -157,8 +156,6 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
157
156
 
158
157
  window.addEventListener('resize', resize)
159
158
  return () => window.removeEventListener('resize', resize)
160
- // We're not checking for all deps, because that will cause rerenders.
161
- // The scroller context shouldn't be changing, but at the moment it is.
162
159
  }, [positions, scrollerRef])
163
160
 
164
161
  // When the overlay is closed by navigating away, we're closing the overlay.
@@ -197,7 +194,13 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
197
194
  useDomEvent(windowRef, 'keyup', handleEscape, { passive: true })
198
195
 
199
196
  // When the overlay isn't visible anymore, we navigate back.
200
- useEffect(() => positions.open.visible.onChange((o) => o === 0 && closeOverlay()))
197
+ useEffect(
198
+ () =>
199
+ positions.open.visible.onChange(
200
+ (o) => position.get() !== OverlayPosition.OPENED && o === 0 && closeOverlay(),
201
+ ),
202
+ [closeOverlay, position, positions.open.visible],
203
+ )
201
204
 
202
205
  // Measure the offset of the overlay in the scroller.
203
206
  const offsetY = useMotionValue(0)
@@ -345,7 +348,6 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
345
348
  gridArea: 'overlay',
346
349
  scrollSnapAlign: 'start',
347
350
  scrollSnapStop: 'always',
348
-
349
351
  [theme.breakpoints.down('md')]: {
350
352
  justifyContent: justifySm,
351
353
  alignItems: justifySm,
@@ -370,6 +372,9 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
370
372
  '&.sizeMdFloating': {
371
373
  padding: `${theme.page.vertical} ${theme.page.horizontal}`,
372
374
  },
375
+ '&.sizeMdFloating.variantMdBottom': {
376
+ marginTop: `calc(${theme.page.vertical} * -1)`,
377
+ },
373
378
  },
374
379
  })}
375
380
  >
@@ -380,7 +385,7 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
380
385
  pointerEvents: 'all',
381
386
  backgroundColor: theme.palette.background.paper,
382
387
  boxShadow: theme.shadows[24],
383
- scrollSnapAlign: 'end',
388
+
384
389
  [theme.breakpoints.down('md')]: {
385
390
  minWidth: '80vw',
386
391
  '&:not(.sizeMdFull)': {
@@ -394,6 +399,7 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
394
399
  '&.variantSmBottom': {
395
400
  borderTopLeftRadius: `${theme.shape.borderRadius * 3}px`,
396
401
  borderTopRightRadius: `${theme.shape.borderRadius * 3}px`,
402
+ scrollSnapAlign: 'end',
397
403
  },
398
404
  '&.sizeSmFloating': {
399
405
  borderRadius: `${theme.shape.borderRadius * 3}px`,
@@ -409,15 +415,11 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
409
415
  },
410
416
  },
411
417
  [theme.breakpoints.up('md')]: {
412
- '&.sizeMdFull': {
413
- minWidth: 'max(600px, 50vw)',
414
- },
415
- '&:not(.sizeMdFull)': {
416
- width: 'max-content',
417
- },
418
+ minWidth: '1px',
418
419
 
419
420
  '&.sizeMdFull.variantMdBottom': {
420
421
  minHeight: `calc(${clientSizeCssVar.y} - ${mdSpacingTop})`,
422
+ scrollSnapAlign: 'end',
421
423
  },
422
424
  '&.sizeMdFull.variantMdLeft': {
423
425
  paddingBottom: '1px',
@@ -433,6 +435,10 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
433
435
  borderTopLeftRadius: `${theme.shape.borderRadius * 4}px`,
434
436
  borderTopRightRadius: `${theme.shape.borderRadius * 4}px`,
435
437
  },
438
+ '&.variantMdLeft, &.variantMdRight': {
439
+ width: 'max-content',
440
+ },
441
+
436
442
  '&.sizeMdFloating': {
437
443
  borderRadius: `${theme.shape.borderRadius * 4}px`,
438
444
  },
@@ -5,71 +5,101 @@ import {
5
5
  useIsomorphicLayoutEffect,
6
6
  } from '@graphcommerce/framer-utils'
7
7
  import { motionValue } from 'framer-motion'
8
- import { useEffect } from 'react'
8
+ import { useCallback, useEffect } from 'react'
9
+ import { useMatchMedia } from '../../hooks'
9
10
 
10
- export function useOverlayPosition() {
11
- const { getScrollSnapPositions, scrollerRef } = useScrollerContext()
11
+ const clampRound = (value: number) => Math.round(Math.max(0, Math.min(1, value)) * 100) / 100
12
12
 
13
+ export function useOverlayPosition(
14
+ variantSm: 'left' | 'bottom' | 'right',
15
+ variantMd: 'left' | 'bottom' | 'right',
16
+ ) {
17
+ const match = useMatchMedia()
18
+ const { getScrollSnapPositions, scrollerRef } = useScrollerContext()
13
19
  const state = useConstant(() => ({
14
20
  open: {
15
21
  x: motionValue(0),
16
22
  y: motionValue(0),
17
23
  visible: motionValue(0),
18
24
  },
19
- closed: { x: motionValue(0), y: motionValue(0) },
25
+ closed: {
26
+ x: motionValue(0),
27
+ y: motionValue(0),
28
+ },
20
29
  }))
21
30
 
22
31
  const scroll = useElementScroll(scrollerRef)
23
32
 
33
+ const variant = useCallback(
34
+ () => (match.up('md') ? variantMd : variantSm),
35
+ [match, variantMd, variantSm],
36
+ )
37
+
24
38
  useIsomorphicLayoutEffect(() => {
25
39
  if (!scrollerRef.current) return () => {}
26
40
 
27
41
  const measure = () => {
28
42
  const positions = getScrollSnapPositions()
29
43
 
30
- state.open.x.set(positions.x[1] ?? 0)
31
- state.closed.x.set(positions.x[0])
32
- state.open.y.set(positions.y[1] ?? 0)
33
- state.closed.y.set(positions.y[0])
44
+ if (variant() === 'left') {
45
+ state.open.x.set(0)
46
+ state.closed.x.set(positions.x[positions.x.length - 1] ?? 0)
47
+ }
48
+ if (variant() === 'right') {
49
+ state.open.x.set(positions.x[positions.x.length - 1] ?? 0)
50
+ state.closed.x.set(0)
51
+ }
52
+ if (variant() === 'bottom') {
53
+ state.open.y.set(positions.y[positions.y.length - 1] ?? 0)
54
+ state.closed.y.set(0)
55
+ }
34
56
  }
35
- const ro = new ResizeObserver(measure)
36
57
  measure()
37
58
 
59
+ const ro = new ResizeObserver(measure)
38
60
  ro.observe(scrollerRef.current)
61
+ ;[...scrollerRef.current.children].forEach((child) => ro.observe(child))
62
+
39
63
  return () => ro.disconnect()
40
- })
64
+ }, [getScrollSnapPositions, scrollerRef, state, variant])
41
65
 
42
66
  // sets a float between 0 and 1 for the visibility of the overlay
43
67
  useEffect(() => {
68
+ if (!scrollerRef.current) return () => {}
44
69
  const calc = () => {
45
- const x = scroll.x.get()
46
- const y = scroll.y.get()
47
-
48
- const yC = state.closed.y.get()
49
- const yO = state.open.y.get()
50
- const visY = yC === yO ? 1 : Math.max(0, Math.min(1, (y - yC) / (yO - yC)))
51
-
52
- const xC = state.closed.x.get()
53
- const xO = state.open.x.get()
70
+ const x = scrollerRef.current?.scrollLeft ?? scroll.x.get()
71
+ const y = scrollerRef.current?.scrollTop ?? scroll.y.get()
54
72
 
55
- const visX = xO === xC ? 1 : Math.max(0, Math.min(1, (x - xC) / (xO - xC)))
56
-
57
- let vis = visY * visX
58
- if (xC === 0 && xO === 0 && yC === 0 && yO === 0) vis = 0
73
+ const positions = getScrollSnapPositions()
59
74
 
60
- // todo: visibility sometimes flickers
61
- state.open.visible.set(vis)
75
+ if (variant() === 'left') {
76
+ const closedX = positions.x[1] ?? 0
77
+ state.open.visible.set(closedX === 0 ? 0 : clampRound((x - closedX) / -closedX))
78
+ }
79
+ if (variant() === 'right') {
80
+ const openedX = positions.x[1] ?? 0
81
+ state.open.visible.set(openedX === 0 ? 0 : clampRound(x / openedX))
82
+ }
83
+ if (variant() === 'bottom') {
84
+ const openedY = positions.y[1] ?? 0
85
+ state.open.visible.set(openedY === 0 ? 0 : clampRound(y / openedY))
86
+ }
62
87
  }
63
88
 
64
89
  const cancelY = scroll.y.onChange(calc)
65
90
  const cancelX = scroll.x.onChange(calc)
66
91
  calc()
67
92
 
93
+ const ro = new ResizeObserver(calc)
94
+ ro.observe(scrollerRef.current)
95
+ ;[...scrollerRef.current.children].forEach((child) => ro.observe(child))
96
+
68
97
  return () => {
69
98
  cancelY()
70
99
  cancelX()
100
+ ro.disconnect()
71
101
  }
72
- }, [state, scroll])
102
+ }, [getScrollSnapPositions, scroll, scrollerRef, state, variant])
73
103
 
74
104
  return state
75
105
  }
@@ -1,20 +1,23 @@
1
1
  import type { OptionalKeysOf, Simplify } from 'type-fest'
2
2
 
3
+ export type RequiredKeys<
4
+ T extends Record<string, unknown>,
5
+ Keys extends OptionalKeysOf<T>,
6
+ > = Simplify<
7
+ Omit<T, Keys> & {
8
+ [K in Keys]: NonNullable<T[K]>
9
+ }
10
+ >
11
+
3
12
  export function filterNonNullableKeys<
4
13
  T extends Record<string, unknown>,
5
14
  Keys extends OptionalKeysOf<T>,
6
15
  >(items: (T | null | undefined)[] | null | undefined, values: Keys[] = []) {
7
16
  if (!items) return []
8
17
 
9
- type ResultWithRequired = Simplify<
10
- Omit<T, Keys> & {
11
- [K in Keys]: NonNullable<T[K]>
12
- }
13
- >
14
-
15
18
  const result = items.filter(
16
19
  (item) => item !== null && typeof item !== 'undefined' && values.every((v) => item?.[v]),
17
20
  )
18
21
 
19
- return result as ResultWithRequired[]
22
+ return result as RequiredKeys<T, Keys>[]
20
23
  }
@@ -1,4 +1,4 @@
1
- import { breakpointVal } from '@graphcommerce/next-ui'
1
+ import { i18n } from '@lingui/core'
2
2
  import {
3
3
  Fab,
4
4
  Snackbar,
@@ -12,7 +12,7 @@ import {
12
12
  } from '@mui/material'
13
13
  import React, { useEffect, useState } from 'react'
14
14
  import { IconSvg } from '../IconSvg'
15
- import { extendableComponent } from '../Styles'
15
+ import { extendableComponent, breakpointVal } from '../Styles'
16
16
  import { iconClose, iconCheckmark, iconSadFace } from '../icons'
17
17
 
18
18
  type Size = 'normal' | 'wide'
@@ -114,7 +114,8 @@ export default function MessageSnackbarImpl(props: MessageSnackbarImplProps) {
114
114
  alignItems: 'center',
115
115
  gap: theme.spacings.xxs,
116
116
  gridTemplate: {
117
- xs: `"icon children close" "action action action"`,
117
+ xs: `"icon children close"
118
+ "action action action"`,
118
119
  md: '"icon children action close"',
119
120
  },
120
121
  gridTemplateColumns: {
@@ -139,7 +140,7 @@ export default function MessageSnackbarImpl(props: MessageSnackbarImplProps) {
139
140
  )}
140
141
  <Fab
141
142
  className={classes.close}
142
- aria-label='Close'
143
+ aria-label={i18n._(/* i18n */ 'Close')}
143
144
  size='small'
144
145
  onClick={hideSnackbar}
145
146
  onMouseDown={preventAnimationBubble}
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/index.ts CHANGED
@@ -1,6 +1,4 @@
1
- export * from './ActionCard/ActionCard'
2
- export * from './ActionCard/ActionCardList'
3
- export * from './ActionCard/ActionCardListForm'
1
+ export * from './ActionCard'
4
2
  export * from './AnimatedRow/AnimatedRow'
5
3
  export * from './Blog/BlogAuthor/BlogAuthor'
6
4
  export * from './Blog/BlogContent/BlogContent'
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.27.0",
5
+ "version": "4.28.1",
6
6
  "author": "",
7
7
  "license": "MIT",
8
8
  "sideEffects": false,
@@ -19,10 +19,10 @@
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.39",
24
- "@graphcommerce/framer-utils": "3.2.0",
25
- "@graphcommerce/image": "3.1.9",
22
+ "@graphcommerce/framer-next-pages": "3.3.2",
23
+ "@graphcommerce/framer-scroller": "2.1.41",
24
+ "@graphcommerce/framer-utils": "3.2.1",
25
+ "@graphcommerce/image": "3.1.10",
26
26
  "cookie": "^0.5.0",
27
27
  "react-is": "^18.2.0",
28
28
  "schema-dts": "^1.1.0"