@graphcommerce/next-ui 4.14.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
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
+
3
21
  ## 4.14.0
4
22
 
5
23
  ### Minor Changes
@@ -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': {
@@ -151,14 +185,35 @@ export function NavigationOverlay(props: NavigationOverlayProps) {
151
185
  sx={(theme) => ({
152
186
  display: 'grid',
153
187
  alignItems: !stretchColumns ? 'start' : undefined,
154
-
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
+ },
155
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,
156
204
  overflow: 'hidden',
157
205
  scrollSnapType: 'x mandatory',
158
- width: `calc(${theme.spacings.md} + ${theme.spacings.md} + ${itemWidth})`,
159
- },
160
- '& .NavigationItem-item': {
161
- 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
+ },
162
217
  },
163
218
  })}
164
219
  >
@@ -228,7 +283,7 @@ export function NavigationOverlay(props: NavigationOverlayProps) {
228
283
  />
229
284
  )}
230
285
 
231
- <NavigationList items={items} selected />
286
+ <NavigationList items={items} selected mouseEvent={mouseEvent} />
232
287
  </Box>
233
288
  </Box>
234
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) => ({
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.14.0",
5
+ "version": "4.15.0",
6
6
  "author": "",
7
7
  "license": "MIT",
8
8
  "sideEffects": false,
@@ -20,7 +20,7 @@
20
20
  "@emotion/server": "^11.4.0",
21
21
  "@emotion/styled": "^11.9.3",
22
22
  "@graphcommerce/framer-next-pages": "3.2.4",
23
- "@graphcommerce/framer-scroller": "2.1.24",
23
+ "@graphcommerce/framer-scroller": "2.1.25",
24
24
  "@graphcommerce/framer-utils": "3.1.4",
25
25
  "@graphcommerce/image": "3.1.7",
26
26
  "cookie": "^0.5.0",