@graphcommerce/next-ui 4.11.1 → 4.13.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/ActionCard/ActionCard.tsx +1 -1
- package/CHANGELOG.md +33 -0
- package/Layout/components/LayoutHeader.tsx +1 -1
- package/Layout/components/LayoutHeaderBack.tsx +1 -1
- package/Layout/components/LayoutHeaderContent.tsx +7 -4
- package/LayoutDefault/components/LayoutDefault.tsx +1 -1
- package/LayoutOverlay/components/LayoutOverlay.tsx +24 -6
- package/LayoutParts/DesktopNavBar.tsx +1 -1
- package/LayoutParts/DesktopNavBarItem.tsx +38 -4
- package/LayoutParts/MenuFabSecondaryItem.tsx +2 -1
- package/Navigation/components/NavigationFab.tsx +106 -0
- package/Navigation/components/NavigationItem.tsx +147 -0
- package/Navigation/components/NavigationList.tsx +50 -0
- package/Navigation/components/NavigationOverlay.tsx +236 -0
- package/Navigation/components/NavigationProvider.tsx +66 -0
- package/Navigation/hooks/useNavigation.ts +58 -0
- package/Navigation/index.ts +4 -0
- package/Overlay/components/Overlay.tsx +61 -0
- package/{LayoutOverlay/components/LayoutOverlayBase.tsx → Overlay/components/OverlayBase.tsx} +29 -15
- package/Overlay/components/index.ts +2 -0
- package/{LayoutOverlay → Overlay}/hooks/useOverlayPosition.ts +0 -0
- package/Overlay/index.ts +1 -0
- package/Theme/DarkLightModeThemeProvider.tsx +1 -5
- package/index.ts +5 -3
- package/package.json +2 -2
|
@@ -82,7 +82,7 @@ export function ActionCard(props: ActionCardProps) {
|
|
|
82
82
|
gridTemplateAreas: `
|
|
83
83
|
"image title action"
|
|
84
84
|
"image details ${price ? 'price' : 'details'}"
|
|
85
|
-
"image
|
|
85
|
+
"image secondaryAction additionalDetails"
|
|
86
86
|
"after after after"
|
|
87
87
|
`,
|
|
88
88
|
justifyContent: 'unset',
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 4.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#1522](https://github.com/graphcommerce-org/graphcommerce/pull/1522) [`8d8fda262`](https://github.com/graphcommerce-org/graphcommerce/commit/8d8fda2623e561cb43441110c67ffa34b692668a) Thanks [@ErwinOtten](https://github.com/ErwinOtten)! - Introducing a new Navigation component that builds on the existing navigation component and tries to address the 'mega menu' question where there are tons of categories that need to be navigated quickly.
|
|
8
|
+
|
|
9
|
+
* [#1522](https://github.com/graphcommerce-org/graphcommerce/pull/1522) [`cefa7b365`](https://github.com/graphcommerce-org/graphcommerce/commit/cefa7b3652b55108d2178927e3c5d98a111cf373) Thanks [@ErwinOtten](https://github.com/ErwinOtten)! - Introducting a new Overlay component with is the generic part of LayoutOverlay into OverlayBase. The new Overlay is used to render the new Navigation component.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [[`584b683a2`](https://github.com/graphcommerce-org/graphcommerce/commit/584b683a2aedcdf5067644c8dcc0e63a5b9e894c)]:
|
|
14
|
+
- @graphcommerce/framer-scroller@2.1.22
|
|
15
|
+
|
|
16
|
+
## 4.12.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- [#1534](https://github.com/graphcommerce-org/graphcommerce/pull/1534) [`c756f42e5`](https://github.com/graphcommerce-org/graphcommerce/commit/c756f42e503761a497e4a5a7a02d02141df231c3) Thanks [@mikekeehnen](https://github.com/mikekeehnen)! - Added the method title to the action card title for shipping methods.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies []:
|
|
25
|
+
- @graphcommerce/framer-scroller@2.1.21
|
|
26
|
+
|
|
27
|
+
## 4.11.2
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- [#1538](https://github.com/graphcommerce-org/graphcommerce/pull/1538) [`fe4baa42d`](https://github.com/graphcommerce-org/graphcommerce/commit/fe4baa42db0081ed960d62aef688bd36a7ac974f) Thanks [@paales](https://github.com/paales)! - add missing translations
|
|
32
|
+
|
|
33
|
+
- Updated dependencies []:
|
|
34
|
+
- @graphcommerce/framer-scroller@2.1.20
|
|
35
|
+
|
|
3
36
|
## 4.11.1
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
|
@@ -46,7 +46,7 @@ export function LayoutHeaderBack(props: BackProps) {
|
|
|
46
46
|
const backIcon = <IconSvg src={iconChevronLeft} size='medium' />
|
|
47
47
|
const canClickBack = backSteps > 0 && path !== prevUp?.href
|
|
48
48
|
|
|
49
|
-
let label = i18n._(/* i18n */
|
|
49
|
+
let label = i18n._(/* i18n */ 'Back')
|
|
50
50
|
if (up?.href === path && up?.title) label = up.title
|
|
51
51
|
if (prevUp?.href === path && prevUp?.title) label = prevUp.title
|
|
52
52
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { useMotionValueValue } from '@graphcommerce/framer-utils'
|
|
2
|
-
import { Box, SxProps, Theme } from '@mui/material'
|
|
2
|
+
import { Box, styled, SxProps, Theme } from '@mui/material'
|
|
3
|
+
import { m } from 'framer-motion'
|
|
3
4
|
import React, { useRef } from 'react'
|
|
4
5
|
import { extendableComponent } from '../../Styles'
|
|
5
6
|
import { useScrollY } from '../hooks/useScrollY'
|
|
6
7
|
import { FloatingProps } from './LayoutHeadertypes'
|
|
7
8
|
|
|
9
|
+
const MotionDiv = styled(m.div)({})
|
|
10
|
+
|
|
8
11
|
export type LayoutHeaderContentProps = FloatingProps & {
|
|
9
12
|
children?: React.ReactNode
|
|
10
13
|
left?: React.ReactNode
|
|
@@ -139,7 +142,7 @@ export function LayoutHeaderContent(props: LayoutHeaderContentProps) {
|
|
|
139
142
|
justifyContent: 'start',
|
|
140
143
|
})}
|
|
141
144
|
>
|
|
142
|
-
{left}
|
|
145
|
+
<MotionDiv layout='position'>{left}</MotionDiv>
|
|
143
146
|
</Box>
|
|
144
147
|
)}
|
|
145
148
|
<Box
|
|
@@ -172,7 +175,7 @@ export function LayoutHeaderContent(props: LayoutHeaderContentProps) {
|
|
|
172
175
|
},
|
|
173
176
|
})}
|
|
174
177
|
>
|
|
175
|
-
{children}
|
|
178
|
+
<MotionDiv layout='position'>{children}</MotionDiv>
|
|
176
179
|
</Box>
|
|
177
180
|
<Box
|
|
178
181
|
className={classes.right}
|
|
@@ -188,7 +191,7 @@ export function LayoutHeaderContent(props: LayoutHeaderContentProps) {
|
|
|
188
191
|
justifyContent: 'end',
|
|
189
192
|
})}
|
|
190
193
|
>
|
|
191
|
-
{right}
|
|
194
|
+
<MotionDiv layout='position'>{right}</MotionDiv>
|
|
192
195
|
</Box>
|
|
193
196
|
{divider && (
|
|
194
197
|
<Box
|
|
@@ -104,7 +104,7 @@ export function LayoutDefault(props: LayoutDefaultProps) {
|
|
|
104
104
|
justifyContent: 'space-between',
|
|
105
105
|
width: '100%',
|
|
106
106
|
height: 0,
|
|
107
|
-
zIndex: '
|
|
107
|
+
zIndex: 'speedDial',
|
|
108
108
|
[theme.breakpoints.up('sm')]: {
|
|
109
109
|
padding: `0 ${theme.page.horizontal}`,
|
|
110
110
|
position: 'sticky',
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { usePageContext, useGo, useScrollOffset } from '@graphcommerce/framer-next-pages'
|
|
1
2
|
import { ScrollerProvider, ScrollSnapType } from '@graphcommerce/framer-scroller'
|
|
3
|
+
import { usePresence } from 'framer-motion'
|
|
2
4
|
import type { SetOptional } from 'type-fest'
|
|
3
|
-
import {
|
|
5
|
+
import { OverlayBase, LayoutOverlayBaseProps } from '../../Overlay/components/OverlayBase'
|
|
4
6
|
|
|
5
|
-
export type
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
export type LayoutOverlayProps = Omit<
|
|
8
|
+
SetOptional<LayoutOverlayBaseProps, 'variantSm' | 'variantMd'>,
|
|
9
|
+
'active' | 'direction' | 'onClosed' | 'offsetPageY' | 'isPresent' | 'safeToRemove'
|
|
10
|
+
>
|
|
8
11
|
|
|
9
12
|
export function LayoutOverlay(props: LayoutOverlayProps) {
|
|
10
13
|
const { children, variantSm = 'bottom', variantMd = 'right', ...otherProps } = props
|
|
@@ -14,11 +17,26 @@ export function LayoutOverlay(props: LayoutOverlayProps) {
|
|
|
14
17
|
const scrollSnapTypeMd: ScrollSnapType =
|
|
15
18
|
variantMd === 'left' || variantMd === 'right' ? 'inline mandatory' : 'block proximity'
|
|
16
19
|
|
|
20
|
+
const { closeSteps, active, direction } = usePageContext()
|
|
21
|
+
const onCloseHandler = useGo(closeSteps * -1)
|
|
22
|
+
const offsetPageY = useScrollOffset().y
|
|
23
|
+
const [isPresent, safeToRemove] = usePresence()
|
|
24
|
+
|
|
17
25
|
return (
|
|
18
26
|
<ScrollerProvider scrollSnapTypeSm={scrollSnapTypeSm} scrollSnapTypeMd={scrollSnapTypeMd}>
|
|
19
|
-
<
|
|
27
|
+
<OverlayBase
|
|
28
|
+
active={active}
|
|
29
|
+
direction={direction}
|
|
30
|
+
onClosed={onCloseHandler}
|
|
31
|
+
offsetPageY={offsetPageY}
|
|
32
|
+
variantMd={variantMd}
|
|
33
|
+
variantSm={variantSm}
|
|
34
|
+
isPresent={isPresent}
|
|
35
|
+
safeToRemove={safeToRemove}
|
|
36
|
+
{...otherProps}
|
|
37
|
+
>
|
|
20
38
|
{children}
|
|
21
|
-
</
|
|
39
|
+
</OverlayBase>
|
|
22
40
|
</ScrollerProvider>
|
|
23
41
|
)
|
|
24
42
|
}
|
|
@@ -43,7 +43,7 @@ export function DesktopNavBar(props: MenuTabsProps) {
|
|
|
43
43
|
sx={(theme) => ({
|
|
44
44
|
gridArea: `1 / 1 / 1 / 4`,
|
|
45
45
|
columnGap: theme.spacings.md,
|
|
46
|
-
padding: `0 ${theme.spacings.
|
|
46
|
+
padding: `0 ${theme.spacings.md}`,
|
|
47
47
|
gridAutoColumns: 'min-content',
|
|
48
48
|
})}
|
|
49
49
|
className={classes.scroller}
|
|
@@ -5,11 +5,45 @@ import { extendableComponent } from '../Styles/extendableComponent'
|
|
|
5
5
|
|
|
6
6
|
const { classes, selectors } = extendableComponent('DesktopNavItem', ['root', 'line'] as const)
|
|
7
7
|
|
|
8
|
-
export type
|
|
8
|
+
export type DesktopNavItemLinkProps = LinkProps<'a'> & Pick<PageLinkProps, 'href'>
|
|
9
|
+
export type DesktopNavItemButtonProps = LinkProps<'div'> & {
|
|
10
|
+
onClick: LinkProps<'button'>['onClick']
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isLinkProps(
|
|
14
|
+
props: DesktopNavItemLinkProps | DesktopNavItemButtonProps,
|
|
15
|
+
): props is DesktopNavItemLinkProps {
|
|
16
|
+
return 'href' in props
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function DesktopNavItem(props: DesktopNavItemLinkProps | DesktopNavItemButtonProps) {
|
|
20
|
+
const router = useRouter()
|
|
21
|
+
|
|
22
|
+
if (!isLinkProps(props)) {
|
|
23
|
+
const { onClick, children, sx = [], ...linkProps } = props
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Link
|
|
27
|
+
className={classes.root}
|
|
28
|
+
component='div'
|
|
29
|
+
variant='h6'
|
|
30
|
+
color='text.primary'
|
|
31
|
+
underline='none'
|
|
32
|
+
{...linkProps}
|
|
33
|
+
onClick={onClick}
|
|
34
|
+
sx={[
|
|
35
|
+
{ whiteSpace: 'nowrap', paddingTop: '6px', cursor: 'pointer' },
|
|
36
|
+
...(Array.isArray(sx) ? sx : [sx]),
|
|
37
|
+
]}
|
|
38
|
+
>
|
|
39
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>{children}</Box>
|
|
40
|
+
</Link>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
9
43
|
|
|
10
|
-
export function DesktopNavItem(props: DesktopNavItemProps) {
|
|
11
44
|
const { href, children, sx = [], ...linkProps } = props
|
|
12
|
-
|
|
45
|
+
|
|
46
|
+
const active = router.asPath.startsWith(href.toString())
|
|
13
47
|
|
|
14
48
|
return (
|
|
15
49
|
<PageLink href={href} passHref>
|
|
@@ -21,7 +55,7 @@ export function DesktopNavItem(props: DesktopNavItemProps) {
|
|
|
21
55
|
{...linkProps}
|
|
22
56
|
sx={[{ whiteSpace: 'nowrap', paddingTop: '6px' }, ...(Array.isArray(sx) ? sx : [sx])]}
|
|
23
57
|
>
|
|
24
|
-
{children}
|
|
58
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>{children}</Box>
|
|
25
59
|
<Box
|
|
26
60
|
component='span'
|
|
27
61
|
className={classes.line}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ListItemButton, ListItemIcon, ListItemText, SxProps, Theme } from '@mui/material'
|
|
2
2
|
import PageLink from 'next/link'
|
|
3
|
-
import
|
|
3
|
+
import { useRouter } from 'next/router'
|
|
4
4
|
import React from 'react'
|
|
5
5
|
import { extendableComponent } from '../Styles'
|
|
6
6
|
|
|
@@ -17,6 +17,7 @@ const { classes } = extendableComponent(compName, parts)
|
|
|
17
17
|
|
|
18
18
|
export function MenuFabSecondaryItem(props: FabMenuSecondaryItemProps) {
|
|
19
19
|
const { href, children, icon, sx = [] } = props
|
|
20
|
+
const router = useRouter()
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
23
|
<PageLink href={href} passHref>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useMotionValueValue } from '@graphcommerce/framer-utils'
|
|
2
|
+
import { Fab, styled, Box, SxProps, Theme, FabProps } from '@mui/material'
|
|
3
|
+
import { m } from 'framer-motion'
|
|
4
|
+
import { useRouter } from 'next/router'
|
|
5
|
+
import React, { useEffect } from 'react'
|
|
6
|
+
import { IconSvg } from '../../IconSvg'
|
|
7
|
+
import { useScrollY } from '../../Layout/hooks/useScrollY'
|
|
8
|
+
import { useFabAnimation } from '../../LayoutParts/useFabAnimation'
|
|
9
|
+
import { extendableComponent } from '../../Styles/extendableComponent'
|
|
10
|
+
import { useFabSize } from '../../Theme'
|
|
11
|
+
import { iconMenu, iconClose } from '../../icons'
|
|
12
|
+
|
|
13
|
+
const MotionDiv = styled(m.div)({})
|
|
14
|
+
|
|
15
|
+
export type NavigationFabProps = {
|
|
16
|
+
menuIcon?: React.ReactNode
|
|
17
|
+
closeIcon?: React.ReactNode
|
|
18
|
+
sx?: SxProps<Theme>
|
|
19
|
+
} & Pick<FabProps, 'color' | 'size' | 'variant' | 'onClick'>
|
|
20
|
+
|
|
21
|
+
const name = 'MenuFab'
|
|
22
|
+
const parts = ['wrapper', 'fab', 'shadow', 'menu'] as const
|
|
23
|
+
type OwnerState = {
|
|
24
|
+
scrolled: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { withState } = extendableComponent<OwnerState, typeof name, typeof parts>(name, parts)
|
|
28
|
+
|
|
29
|
+
export function NavigationFab(props: NavigationFabProps) {
|
|
30
|
+
const { menuIcon, closeIcon, sx = [], ...fabProps } = props
|
|
31
|
+
const router = useRouter()
|
|
32
|
+
const [openEl, setOpenEl] = React.useState<null | HTMLElement>(null)
|
|
33
|
+
|
|
34
|
+
const { opacity, shadowOpacity } = useFabAnimation()
|
|
35
|
+
const scrollY = useScrollY()
|
|
36
|
+
const scrolled = useMotionValueValue(scrollY, (y) => y > 10)
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const clear = () => setOpenEl(null)
|
|
40
|
+
router.events.on('routeChangeStart', clear)
|
|
41
|
+
return () => router.events.off('routeChangeStart', clear)
|
|
42
|
+
}, [router.events])
|
|
43
|
+
|
|
44
|
+
const fabIconSize = useFabSize('responsive')
|
|
45
|
+
|
|
46
|
+
const classes = withState({ scrolled })
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Box
|
|
50
|
+
sx={[
|
|
51
|
+
{ position: 'relative', width: fabIconSize, height: fabIconSize },
|
|
52
|
+
...(Array.isArray(sx) ? sx : [sx]),
|
|
53
|
+
]}
|
|
54
|
+
>
|
|
55
|
+
<MotionDiv
|
|
56
|
+
className={classes.wrapper}
|
|
57
|
+
sx={(theme) => ({
|
|
58
|
+
[theme.breakpoints.down('md')]: {
|
|
59
|
+
opacity: '1 !important',
|
|
60
|
+
transform: 'none !important',
|
|
61
|
+
},
|
|
62
|
+
})}
|
|
63
|
+
style={{ opacity }}
|
|
64
|
+
>
|
|
65
|
+
<Fab
|
|
66
|
+
color='inherit'
|
|
67
|
+
aria-label='Open Menu'
|
|
68
|
+
size='responsive'
|
|
69
|
+
sx={(theme) => ({
|
|
70
|
+
boxShadow: 'none',
|
|
71
|
+
'&:hover, &:focus': {
|
|
72
|
+
boxShadow: 'none',
|
|
73
|
+
background: theme.palette.text.primary,
|
|
74
|
+
},
|
|
75
|
+
background: theme.palette.text.primary,
|
|
76
|
+
pointerEvents: 'all',
|
|
77
|
+
color: theme.palette.background.paper,
|
|
78
|
+
})}
|
|
79
|
+
className={classes.fab}
|
|
80
|
+
{...fabProps}
|
|
81
|
+
>
|
|
82
|
+
{closeIcon ?? (
|
|
83
|
+
<IconSvg src={iconClose} size='large' sx={{ display: openEl ? 'block' : 'none' }} />
|
|
84
|
+
)}
|
|
85
|
+
{menuIcon ?? (
|
|
86
|
+
<IconSvg src={iconMenu} size='large' sx={{ display: openEl ? 'none' : 'block' }} />
|
|
87
|
+
)}
|
|
88
|
+
</Fab>
|
|
89
|
+
<MotionDiv
|
|
90
|
+
sx={(theme) => ({
|
|
91
|
+
pointerEvents: 'none',
|
|
92
|
+
borderRadius: '99em',
|
|
93
|
+
position: 'absolute',
|
|
94
|
+
height: '100%',
|
|
95
|
+
width: '100%',
|
|
96
|
+
boxShadow: theme.shadows[6],
|
|
97
|
+
top: 0,
|
|
98
|
+
[theme.breakpoints.down('md')]: { opacity: '1 !important' },
|
|
99
|
+
})}
|
|
100
|
+
className={classes.shadow}
|
|
101
|
+
style={{ opacity: shadowOpacity }}
|
|
102
|
+
/>
|
|
103
|
+
</MotionDiv>
|
|
104
|
+
</Box>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
2
|
+
import { Box, ListItemButton, styled, useEventCallback } from '@mui/material'
|
|
3
|
+
import PageLink from 'next/link'
|
|
4
|
+
import { IconSvg } from '../../IconSvg'
|
|
5
|
+
import { extendableComponent } from '../../Styles/extendableComponent'
|
|
6
|
+
import { iconChevronRight } from '../../icons'
|
|
7
|
+
import {
|
|
8
|
+
isNavigationButton,
|
|
9
|
+
isNavigationComponent,
|
|
10
|
+
isNavigationHref,
|
|
11
|
+
NavigationNode,
|
|
12
|
+
NavigationPath,
|
|
13
|
+
useNavigation,
|
|
14
|
+
} from '../hooks/useNavigation'
|
|
15
|
+
import type { NavigationList } from './NavigationList'
|
|
16
|
+
|
|
17
|
+
type OwnerState = {
|
|
18
|
+
first?: boolean
|
|
19
|
+
last?: boolean
|
|
20
|
+
// It is actually used.
|
|
21
|
+
// eslint-disable-next-line react/no-unused-prop-types
|
|
22
|
+
column: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type NavigationItemProps = NavigationNode & {
|
|
26
|
+
parentPath: NavigationPath
|
|
27
|
+
idx: number
|
|
28
|
+
NavigationList: typeof NavigationList
|
|
29
|
+
} & OwnerState
|
|
30
|
+
|
|
31
|
+
const componentName = 'NavigationItem'
|
|
32
|
+
const parts = ['li', 'ul', 'item'] as const
|
|
33
|
+
|
|
34
|
+
const { withState } = extendableComponent<OwnerState, typeof componentName, typeof parts>(
|
|
35
|
+
componentName,
|
|
36
|
+
parts,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const NavigationLI = styled('li')({ display: 'contents' })
|
|
40
|
+
|
|
41
|
+
export function NavigationItem(props: NavigationItemProps) {
|
|
42
|
+
const { id, parentPath, idx, first, last, NavigationList } = props
|
|
43
|
+
|
|
44
|
+
const row = idx + 1
|
|
45
|
+
const { selected, select, hideRootOnNavigate, onClose } = useNavigation()
|
|
46
|
+
|
|
47
|
+
const itemPath = [...parentPath, id]
|
|
48
|
+
const isSelected = selected.slice(0, itemPath.length).join('/') === itemPath.join('/')
|
|
49
|
+
|
|
50
|
+
const hidingRoot = hideRootOnNavigate && selected.length > 0
|
|
51
|
+
const hideItem = hidingRoot && itemPath.length === 1
|
|
52
|
+
|
|
53
|
+
const column = hidingRoot ? itemPath.length - 1 : itemPath.length
|
|
54
|
+
const classes = withState({ first, last, column: itemPath.length })
|
|
55
|
+
|
|
56
|
+
const onCloseHandler: React.MouseEventHandler<HTMLAnchorElement> = useEventCallback((e) => {
|
|
57
|
+
if (!isNavigationHref(props)) return
|
|
58
|
+
const { href } = props
|
|
59
|
+
onClose?.(e, href)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (isNavigationButton(props)) {
|
|
63
|
+
const { childItems, name } = props
|
|
64
|
+
return (
|
|
65
|
+
<NavigationLI className={classes.li}>
|
|
66
|
+
<ListItemButton
|
|
67
|
+
className={classes.item}
|
|
68
|
+
role='button'
|
|
69
|
+
sx={{
|
|
70
|
+
gridRowStart: row,
|
|
71
|
+
gridColumnStart: column,
|
|
72
|
+
gap: (theme) => theme.spacings.xxs,
|
|
73
|
+
display: hideItem ? 'none' : 'flex',
|
|
74
|
+
}}
|
|
75
|
+
disabled={isSelected}
|
|
76
|
+
tabIndex={selected.join(',').includes(parentPath.join(',')) ? undefined : -1}
|
|
77
|
+
onClick={(e) => {
|
|
78
|
+
e.preventDefault()
|
|
79
|
+
if (!isSelected) select(itemPath)
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<Box
|
|
83
|
+
component='span'
|
|
84
|
+
sx={{
|
|
85
|
+
whiteSpace: 'nowrap',
|
|
86
|
+
overflowX: 'hidden',
|
|
87
|
+
textOverflow: 'ellipsis',
|
|
88
|
+
flexGrow: 1,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{name}
|
|
92
|
+
</Box>
|
|
93
|
+
<IconSvg src={iconChevronRight} sx={{ flexShrink: 0 }} />
|
|
94
|
+
</ListItemButton>
|
|
95
|
+
|
|
96
|
+
<NavigationList items={childItems} selected={isSelected} parentPath={itemPath} />
|
|
97
|
+
</NavigationLI>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isNavigationHref(props)) {
|
|
102
|
+
const { name, href } = props
|
|
103
|
+
return (
|
|
104
|
+
<NavigationLI sx={[hideItem && { display: 'none' }]} className={classes.li}>
|
|
105
|
+
<PageLink href={href} passHref>
|
|
106
|
+
<ListItemButton
|
|
107
|
+
className={classes.item}
|
|
108
|
+
component='a'
|
|
109
|
+
sx={(theme) => ({
|
|
110
|
+
gridRowStart: row,
|
|
111
|
+
gridColumnStart: column,
|
|
112
|
+
gap: theme.spacings.xxs,
|
|
113
|
+
})}
|
|
114
|
+
tabIndex={selected.join(',').includes(parentPath.join(',')) ? undefined : -1}
|
|
115
|
+
onClick={onCloseHandler}
|
|
116
|
+
>
|
|
117
|
+
<Box
|
|
118
|
+
component='span'
|
|
119
|
+
sx={{
|
|
120
|
+
whiteSpace: 'nowrap',
|
|
121
|
+
overflowX: 'hidden',
|
|
122
|
+
textOverflow: 'ellipsis',
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{name}
|
|
126
|
+
</Box>
|
|
127
|
+
</ListItemButton>
|
|
128
|
+
</PageLink>
|
|
129
|
+
</NavigationLI>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isNavigationComponent(props)) {
|
|
134
|
+
const { component } = props
|
|
135
|
+
return (
|
|
136
|
+
<NavigationLI sx={[hideItem && { display: 'none' }]} className={classes.li}>
|
|
137
|
+
<Box sx={{ gridRowStart: row, gridColumnStart: column }} className={classes.item}>
|
|
138
|
+
{component}
|
|
139
|
+
</Box>
|
|
140
|
+
</NavigationLI>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (process.env.NODE_ENV !== 'production') throw Error('NavigationItem: unknown type')
|
|
145
|
+
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { styled } from '@mui/material'
|
|
2
|
+
import { extendableComponent } from '../../Styles/extendableComponent'
|
|
3
|
+
import { NavigationNode, NavigationPath } from '../hooks/useNavigation'
|
|
4
|
+
import { NavigationItem } from './NavigationItem'
|
|
5
|
+
|
|
6
|
+
const NavigationUList = styled('ul')({})
|
|
7
|
+
|
|
8
|
+
type NavigationItemsProps = {
|
|
9
|
+
parentPath?: NavigationPath
|
|
10
|
+
items: NavigationNode[]
|
|
11
|
+
selected?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type OwnerState = {
|
|
15
|
+
column: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const name = 'NavigationList'
|
|
19
|
+
const parts = ['root'] as const
|
|
20
|
+
const { withState } = extendableComponent<OwnerState, typeof name, typeof parts>(name, parts)
|
|
21
|
+
|
|
22
|
+
// const componentName = 'NavigationItem'
|
|
23
|
+
// const parts = ['li', 'ul', 'item'] as const
|
|
24
|
+
|
|
25
|
+
export function NavigationList(props: NavigationItemsProps) {
|
|
26
|
+
const { items, parentPath = [], selected = false } = props
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<NavigationUList
|
|
30
|
+
sx={[
|
|
31
|
+
{ display: 'block', position: 'absolute', left: '-10000px', top: '-10000px' },
|
|
32
|
+
selected && { display: 'contents' },
|
|
33
|
+
]}
|
|
34
|
+
className={withState({ column: 0 }).root}
|
|
35
|
+
>
|
|
36
|
+
{items.map((item, idx) => (
|
|
37
|
+
<NavigationItem
|
|
38
|
+
NavigationList={NavigationList}
|
|
39
|
+
key={item.id}
|
|
40
|
+
{...item}
|
|
41
|
+
parentPath={parentPath}
|
|
42
|
+
idx={idx}
|
|
43
|
+
first={idx === 0}
|
|
44
|
+
last={idx === items.length - 1}
|
|
45
|
+
column={0}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</NavigationUList>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import styled from '@emotion/styled'
|
|
2
|
+
import { i18n } from '@lingui/core'
|
|
3
|
+
import { Trans } from '@lingui/react'
|
|
4
|
+
import { Box, Fab, SxProps, Theme, useEventCallback, useMediaQuery } from '@mui/material'
|
|
5
|
+
import { m } from 'framer-motion'
|
|
6
|
+
import { IconSvg, useIconSvgSize } from '../../IconSvg'
|
|
7
|
+
import { LayoutHeaderContent } from '../../Layout/components/LayoutHeaderContent'
|
|
8
|
+
import { LayoutTitle } from '../../Layout/components/LayoutTitle'
|
|
9
|
+
import { Overlay } from '../../Overlay/components/Overlay'
|
|
10
|
+
import { extendableComponent } from '../../Styles/extendableComponent'
|
|
11
|
+
import { useFabSize } from '../../Theme'
|
|
12
|
+
import { iconClose, iconChevronLeft } from '../../icons'
|
|
13
|
+
import {
|
|
14
|
+
isNavigationButton,
|
|
15
|
+
isNavigationComponent,
|
|
16
|
+
NavigationContextType,
|
|
17
|
+
NavigationNodeButton,
|
|
18
|
+
NavigationNodeHref,
|
|
19
|
+
useNavigation,
|
|
20
|
+
} from '../hooks/useNavigation'
|
|
21
|
+
import { NavigationList } from './NavigationList'
|
|
22
|
+
|
|
23
|
+
type NavigationOverlayProps = {
|
|
24
|
+
active: boolean
|
|
25
|
+
sx?: SxProps<Theme>
|
|
26
|
+
stretchColumns?: boolean
|
|
27
|
+
itemWidth: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findCurrent(
|
|
31
|
+
items: NavigationContextType['items'],
|
|
32
|
+
selected: NavigationContextType['selected'],
|
|
33
|
+
): NavigationNodeHref | NavigationNodeButton | undefined {
|
|
34
|
+
const lastItem = selected.slice(-1)[0]
|
|
35
|
+
|
|
36
|
+
if (!lastItem) return undefined
|
|
37
|
+
|
|
38
|
+
for (const item of items) {
|
|
39
|
+
// eslint-disable-next-line no-continue
|
|
40
|
+
if (isNavigationComponent(item)) continue
|
|
41
|
+
|
|
42
|
+
// If the item is the current one, return it
|
|
43
|
+
if (item.id === lastItem) return item
|
|
44
|
+
|
|
45
|
+
// Recursively find item
|
|
46
|
+
if (isNavigationButton(item)) return findCurrent(item.childItems, selected)
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MotionDiv = styled(m.div)()
|
|
52
|
+
|
|
53
|
+
const componentName = 'Navigation'
|
|
54
|
+
const parts = ['root', 'navigation', 'header', 'column'] as const
|
|
55
|
+
const { classes } = extendableComponent(componentName, parts)
|
|
56
|
+
|
|
57
|
+
export function NavigationOverlay(props: NavigationOverlayProps) {
|
|
58
|
+
const { active, sx, stretchColumns, itemWidth } = props
|
|
59
|
+
const { selected, select, items, onClose } = useNavigation()
|
|
60
|
+
|
|
61
|
+
const fabSize = useFabSize('responsive')
|
|
62
|
+
const svgSize = useIconSvgSize('large')
|
|
63
|
+
|
|
64
|
+
const isMobile = useMediaQuery<Theme>((theme) => theme.breakpoints.down('md'))
|
|
65
|
+
const handleOnBack = useEventCallback(() => {
|
|
66
|
+
if (isMobile) select(selected.slice(0, -1))
|
|
67
|
+
else select([])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const showBack = selected.length > 0
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Overlay
|
|
74
|
+
className={classes.root}
|
|
75
|
+
active={active}
|
|
76
|
+
onClosed={onClose}
|
|
77
|
+
variantSm='left'
|
|
78
|
+
sizeSm='floating'
|
|
79
|
+
justifySm='start'
|
|
80
|
+
variantMd='left'
|
|
81
|
+
sizeMd='floating'
|
|
82
|
+
justifyMd='start'
|
|
83
|
+
sx={{
|
|
84
|
+
zIndex: 'drawer',
|
|
85
|
+
'& .LayoutOverlayBase-overlayPane': {
|
|
86
|
+
minWidth: 'auto !important',
|
|
87
|
+
width: 'max-content',
|
|
88
|
+
overflow: 'hidden',
|
|
89
|
+
display: 'grid',
|
|
90
|
+
gridTemplateRows: 'auto 1fr',
|
|
91
|
+
},
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<MotionDiv layout style={{ display: 'grid' }}>
|
|
95
|
+
<Box
|
|
96
|
+
className={classes.header}
|
|
97
|
+
sx={(theme) => ({
|
|
98
|
+
top: 0,
|
|
99
|
+
position: 'sticky',
|
|
100
|
+
height: { xs: theme.appShell.headerHeightSm, md: theme.appShell.appBarHeightMd },
|
|
101
|
+
zIndex: 1,
|
|
102
|
+
})}
|
|
103
|
+
>
|
|
104
|
+
<LayoutHeaderContent
|
|
105
|
+
floatingMd={false}
|
|
106
|
+
floatingSm={false}
|
|
107
|
+
switchPoint={0}
|
|
108
|
+
left={
|
|
109
|
+
showBack && (
|
|
110
|
+
<Fab
|
|
111
|
+
color='inherit'
|
|
112
|
+
onClick={handleOnBack}
|
|
113
|
+
sx={{
|
|
114
|
+
boxShadow: 'none',
|
|
115
|
+
marginLeft: `calc((${fabSize} - ${svgSize}) * -0.5)`,
|
|
116
|
+
marginRight: `calc((${fabSize} - ${svgSize}) * -0.5)`,
|
|
117
|
+
}}
|
|
118
|
+
size='responsive'
|
|
119
|
+
aria-label={i18n._(/* i18n */ 'Back')}
|
|
120
|
+
>
|
|
121
|
+
<IconSvg src={iconChevronLeft} size='large' aria-hidden />
|
|
122
|
+
</Fab>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
right={
|
|
126
|
+
<Fab
|
|
127
|
+
color='inherit'
|
|
128
|
+
onClick={() => onClose()}
|
|
129
|
+
sx={{
|
|
130
|
+
boxShadow: 'none',
|
|
131
|
+
marginLeft: `calc((${fabSize} - ${svgSize}) * -0.5)`,
|
|
132
|
+
marginRight: `calc((${fabSize} - ${svgSize}) * -0.5)`,
|
|
133
|
+
}}
|
|
134
|
+
size='responsive'
|
|
135
|
+
aria-label={i18n._(/* i18n */ 'Close')}
|
|
136
|
+
>
|
|
137
|
+
<IconSvg src={iconClose} size='large' aria-hidden />
|
|
138
|
+
</Fab>
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
<LayoutTitle size='small' component='span'>
|
|
142
|
+
{findCurrent(items, selected)?.name ?? <Trans id='Menu' />}
|
|
143
|
+
</LayoutTitle>
|
|
144
|
+
</LayoutHeaderContent>
|
|
145
|
+
</Box>
|
|
146
|
+
</MotionDiv>
|
|
147
|
+
|
|
148
|
+
<MotionDiv layout='position' style={{ display: 'grid' }}>
|
|
149
|
+
<Box
|
|
150
|
+
sx={(theme) => ({
|
|
151
|
+
display: 'grid',
|
|
152
|
+
alignItems: !stretchColumns ? 'start' : undefined,
|
|
153
|
+
|
|
154
|
+
[theme.breakpoints.down('md')]: {
|
|
155
|
+
overflow: 'hidden',
|
|
156
|
+
scrollSnapType: 'x mandatory',
|
|
157
|
+
width: `calc(${theme.spacings.md} + ${theme.spacings.md} + ${itemWidth})`,
|
|
158
|
+
},
|
|
159
|
+
'& .NavigationItem-item': {
|
|
160
|
+
width: itemWidth,
|
|
161
|
+
},
|
|
162
|
+
})}
|
|
163
|
+
>
|
|
164
|
+
<Box
|
|
165
|
+
className={classes.navigation}
|
|
166
|
+
sx={[
|
|
167
|
+
(theme) => ({
|
|
168
|
+
py: theme.spacings.md,
|
|
169
|
+
display: 'grid',
|
|
170
|
+
gridAutoFlow: 'column',
|
|
171
|
+
scrollSnapAlign: 'end',
|
|
172
|
+
'& > ul > li > a, & > ul > li > [role=button]': {
|
|
173
|
+
'& span': {
|
|
174
|
+
typography: 'h2',
|
|
175
|
+
},
|
|
176
|
+
// '& svg': { display: 'none' },
|
|
177
|
+
},
|
|
178
|
+
'& .Navigation-column': {},
|
|
179
|
+
'& .NavigationItem-item': {
|
|
180
|
+
mx: theme.spacings.md,
|
|
181
|
+
whiteSpace: 'nowrap',
|
|
182
|
+
},
|
|
183
|
+
'& .NavigationItem-item.first': {
|
|
184
|
+
// mt: theme.spacings.md,
|
|
185
|
+
},
|
|
186
|
+
'& .Navigation-column:first-of-type': {
|
|
187
|
+
boxShadow: 'none',
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
...(Array.isArray(sx) ? sx : [sx]),
|
|
191
|
+
]}
|
|
192
|
+
>
|
|
193
|
+
{selected.length >= 0 && (
|
|
194
|
+
<Box
|
|
195
|
+
sx={(theme) => ({
|
|
196
|
+
gridArea: '1 / 1 / 999 / 2',
|
|
197
|
+
boxShadow: `inset 1px 0 ${theme.palette.divider}`,
|
|
198
|
+
})}
|
|
199
|
+
className={classes.column}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
{selected.length >= 1 && (
|
|
203
|
+
<Box
|
|
204
|
+
sx={(theme) => ({
|
|
205
|
+
gridArea: '1 / 2 / 999 / 3',
|
|
206
|
+
boxShadow: `inset 1px 0 ${theme.palette.divider}`,
|
|
207
|
+
})}
|
|
208
|
+
className={classes.column}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
{selected.length >= 2 && (
|
|
212
|
+
<Box
|
|
213
|
+
sx={(theme) => ({
|
|
214
|
+
gridArea: '1 / 3 / 999 / 4',
|
|
215
|
+
boxShadow: `inset 1px 0 ${theme.palette.divider}`,
|
|
216
|
+
})}
|
|
217
|
+
className={classes.column}
|
|
218
|
+
/>
|
|
219
|
+
)}
|
|
220
|
+
{selected.length >= 3 && (
|
|
221
|
+
<Box
|
|
222
|
+
sx={(theme) => ({
|
|
223
|
+
gridArea: '1 / 4 / 999 / 5',
|
|
224
|
+
boxShadow: `inset 1px 0 ${theme.palette.divider}`,
|
|
225
|
+
})}
|
|
226
|
+
className={classes.column}
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
<NavigationList items={items} selected />
|
|
231
|
+
</Box>
|
|
232
|
+
</Box>
|
|
233
|
+
</MotionDiv>
|
|
234
|
+
</Overlay>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useEventCallback } from '@mui/material'
|
|
2
|
+
import { MotionConfig } from 'framer-motion'
|
|
3
|
+
import { useState, useMemo } from 'react'
|
|
4
|
+
import { isElement } from 'react-is'
|
|
5
|
+
import {
|
|
6
|
+
NavigationNode,
|
|
7
|
+
NavigationPath,
|
|
8
|
+
NavigationContextType,
|
|
9
|
+
NavigationContext,
|
|
10
|
+
NavigationSelect,
|
|
11
|
+
} from '../hooks/useNavigation'
|
|
12
|
+
|
|
13
|
+
export type NavigationProviderProps = {
|
|
14
|
+
items: (NavigationNode | React.ReactElement)[]
|
|
15
|
+
hideRootOnNavigate?: boolean
|
|
16
|
+
closeAfterNavigate?: boolean
|
|
17
|
+
children?: React.ReactNode
|
|
18
|
+
animationDuration?: number
|
|
19
|
+
onChange?: NavigationSelect
|
|
20
|
+
onClose?: NavigationContextType['onClose']
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nonNullable = <T,>(value: T): value is NonNullable<T> => value !== null && value !== undefined
|
|
24
|
+
|
|
25
|
+
export function NavigationProvider(props: NavigationProviderProps) {
|
|
26
|
+
const {
|
|
27
|
+
items,
|
|
28
|
+
onChange,
|
|
29
|
+
hideRootOnNavigate = true,
|
|
30
|
+
closeAfterNavigate = false,
|
|
31
|
+
animationDuration = 0.275,
|
|
32
|
+
children,
|
|
33
|
+
onClose: onCloseUnstable,
|
|
34
|
+
} = props
|
|
35
|
+
|
|
36
|
+
const [selected, setSelected] = useState<NavigationPath>([])
|
|
37
|
+
|
|
38
|
+
const select = useEventCallback((incomming: NavigationPath) => {
|
|
39
|
+
setSelected(incomming)
|
|
40
|
+
onChange?.(incomming)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const onClose: NavigationContextType['onClose'] = useEventCallback((e, href) => {
|
|
44
|
+
onCloseUnstable?.(e, href)
|
|
45
|
+
setTimeout(() => select([]), animationDuration * 1000)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const value = useMemo<NavigationContextType>(
|
|
49
|
+
() => ({
|
|
50
|
+
hideRootOnNavigate,
|
|
51
|
+
selected,
|
|
52
|
+
select,
|
|
53
|
+
items: items
|
|
54
|
+
.map((item, index) => (isElement(item) ? { id: item.key ?? index, component: item } : item))
|
|
55
|
+
.filter(nonNullable),
|
|
56
|
+
onClose,
|
|
57
|
+
}),
|
|
58
|
+
[hideRootOnNavigate, selected, select, items, onClose],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<MotionConfig transition={{ duration: animationDuration }}>
|
|
63
|
+
<NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>
|
|
64
|
+
</MotionConfig>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
export type NavigationId = string | number
|
|
4
|
+
export type NavigationPath = NavigationId[]
|
|
5
|
+
export type NavigationSelect = (selected: NavigationPath) => void
|
|
6
|
+
export type NavigationRender = React.FC<
|
|
7
|
+
(NavigationNodeComponent | NavigationNodeHref) & { children?: React.ReactNode }
|
|
8
|
+
>
|
|
9
|
+
|
|
10
|
+
export type NavigationOnClose = (
|
|
11
|
+
event?: React.MouseEvent<HTMLAnchorElement>,
|
|
12
|
+
href?: string | undefined,
|
|
13
|
+
) => void
|
|
14
|
+
export type NavigationContextType = {
|
|
15
|
+
selected: NavigationPath
|
|
16
|
+
select: NavigationSelect
|
|
17
|
+
items: NavigationNode[]
|
|
18
|
+
hideRootOnNavigate: boolean
|
|
19
|
+
onClose: NavigationOnClose
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type NavigationNodeBase = {
|
|
23
|
+
id: NavigationId
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type NavigationNodeHref = NavigationNodeBase & {
|
|
27
|
+
name: string
|
|
28
|
+
href: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type NavigationNodeButton = NavigationNodeBase & {
|
|
32
|
+
name: string
|
|
33
|
+
childItems: NavigationNode[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type NavigationNodeComponent = NavigationNodeBase & {
|
|
37
|
+
component: React.ReactNode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type NavigationNode = NavigationNodeHref | NavigationNodeButton | NavigationNodeComponent
|
|
41
|
+
|
|
42
|
+
export function isNavigationHref(node: NavigationNodeBase): node is NavigationNodeHref {
|
|
43
|
+
return 'href' in node
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isNavigationButton(node: NavigationNodeBase): node is NavigationNodeButton {
|
|
47
|
+
return (node as NavigationNodeButton).childItems?.length > 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isNavigationComponent(node: NavigationNodeBase): node is NavigationNodeComponent {
|
|
51
|
+
return 'component' in node
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const NavigationContext = createContext(undefined as unknown as NavigationContextType)
|
|
55
|
+
|
|
56
|
+
export function useNavigation() {
|
|
57
|
+
return useContext(NavigationContext)
|
|
58
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ScrollerProvider, ScrollSnapType } from '@graphcommerce/framer-scroller'
|
|
2
|
+
import { Box } from '@mui/material'
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import type { SetOptional } from 'type-fest'
|
|
5
|
+
import { OverlayBase, LayoutOverlayBaseProps } from './OverlayBase'
|
|
6
|
+
|
|
7
|
+
export type OverlayProps = Omit<
|
|
8
|
+
SetOptional<LayoutOverlayBaseProps, 'variantSm' | 'variantMd'>,
|
|
9
|
+
'direction' | 'offsetPageY' | 'isPresent' | 'safeToRemove'
|
|
10
|
+
>
|
|
11
|
+
|
|
12
|
+
export function Overlay(props: OverlayProps) {
|
|
13
|
+
const { children, variantSm = 'bottom', variantMd = 'right', active, ...otherProps } = props
|
|
14
|
+
|
|
15
|
+
const scrollSnapTypeSm: ScrollSnapType =
|
|
16
|
+
variantSm === 'left' || variantSm === 'right' ? 'inline mandatory' : 'block proximity'
|
|
17
|
+
const scrollSnapTypeMd: ScrollSnapType =
|
|
18
|
+
variantMd === 'left' || variantMd === 'right' ? 'inline mandatory' : 'block proximity'
|
|
19
|
+
|
|
20
|
+
// todo: The overlay is always present in the DOM and the initial scroll position is set to 0.
|
|
21
|
+
// This means in this case that the overlay is visisble. LayoutOverlayBase sets the scroll position to 320 with JS.
|
|
22
|
+
// This would cause the overlay to be visisble for a moment before the scroll position is set.
|
|
23
|
+
// The solution is to set the the first scroll-snap-align and scroll-snap-stop to the open position of the overlay.
|
|
24
|
+
// However: We have control of the LayoutOverlayBase, we do not have control of all the child components so that solution will not work..
|
|
25
|
+
const [loaded, setLoaded] = useState(false)
|
|
26
|
+
useEffect(() => setLoaded(true), [])
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Box
|
|
30
|
+
className='Overlay'
|
|
31
|
+
sx={{
|
|
32
|
+
position: 'fixed',
|
|
33
|
+
top: 0,
|
|
34
|
+
left: 0,
|
|
35
|
+
transform: loaded ? undefined : 'translateX(-200vw)',
|
|
36
|
+
pointerEvents: active ? undefined : 'none',
|
|
37
|
+
right: 0,
|
|
38
|
+
bottom: 0,
|
|
39
|
+
zIndex: 'drawer',
|
|
40
|
+
'& .LayoutOverlayBase-overlayPane': {
|
|
41
|
+
boxShadow: active ? undefined : 0,
|
|
42
|
+
},
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<ScrollerProvider scrollSnapTypeSm={scrollSnapTypeSm} scrollSnapTypeMd={scrollSnapTypeMd}>
|
|
46
|
+
<OverlayBase
|
|
47
|
+
offsetPageY={0}
|
|
48
|
+
variantMd={variantMd}
|
|
49
|
+
variantSm={variantSm}
|
|
50
|
+
direction={1}
|
|
51
|
+
active={active}
|
|
52
|
+
isPresent={active}
|
|
53
|
+
safeToRemove={undefined}
|
|
54
|
+
{...otherProps}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</OverlayBase>
|
|
58
|
+
</ScrollerProvider>
|
|
59
|
+
</Box>
|
|
60
|
+
)
|
|
61
|
+
}
|
package/{LayoutOverlay/components/LayoutOverlayBase.tsx → Overlay/components/OverlayBase.tsx}
RENAMED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useGo, usePageContext, useScrollOffset } from '@graphcommerce/framer-next-pages'
|
|
2
1
|
import { Scroller, useScrollerContext, useScrollTo } from '@graphcommerce/framer-scroller'
|
|
3
2
|
import {
|
|
4
3
|
clientSizeCssVar,
|
|
@@ -6,7 +5,7 @@ import {
|
|
|
6
5
|
useIsomorphicLayoutEffect,
|
|
7
6
|
} from '@graphcommerce/framer-utils'
|
|
8
7
|
import { Box, styled, SxProps, Theme, useTheme, useThemeProps } from '@mui/material'
|
|
9
|
-
import { m, useDomEvent, useMotionValue,
|
|
8
|
+
import { m, useDomEvent, useMotionValue, useTransform } from 'framer-motion'
|
|
10
9
|
import React, { useCallback, useEffect, useRef } from 'react'
|
|
11
10
|
import { LayoutProvider } from '../../Layout/components/LayoutProvider'
|
|
12
11
|
import { ExtendableComponent, extendableComponent } from '../../Styles'
|
|
@@ -35,6 +34,12 @@ export type LayoutOverlayBaseProps = {
|
|
|
35
34
|
className?: string
|
|
36
35
|
sx?: SxProps<Theme>
|
|
37
36
|
sxBackdrop?: SxProps<Theme>
|
|
37
|
+
active: boolean
|
|
38
|
+
direction: 1 | -1
|
|
39
|
+
onClosed: () => void
|
|
40
|
+
offsetPageY: number
|
|
41
|
+
isPresent: boolean
|
|
42
|
+
safeToRemove: (() => void) | null | undefined
|
|
38
43
|
} & StyleProps &
|
|
39
44
|
OverridableProps
|
|
40
45
|
|
|
@@ -61,7 +66,7 @@ const clearScrollLock = () => {
|
|
|
61
66
|
document.body.style.overflow = ''
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
export function
|
|
69
|
+
export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
|
|
65
70
|
const props = useThemeProps({ name, props: incommingProps })
|
|
66
71
|
|
|
67
72
|
const {
|
|
@@ -75,6 +80,12 @@ export function LayoutOverlayBase(incommingProps: LayoutOverlayBaseProps) {
|
|
|
75
80
|
justifyMd = 'stretch',
|
|
76
81
|
sx = [],
|
|
77
82
|
sxBackdrop = [],
|
|
83
|
+
active,
|
|
84
|
+
onClosed,
|
|
85
|
+
direction,
|
|
86
|
+
offsetPageY,
|
|
87
|
+
isPresent,
|
|
88
|
+
safeToRemove,
|
|
78
89
|
} = props
|
|
79
90
|
|
|
80
91
|
const th = useTheme()
|
|
@@ -88,12 +99,8 @@ export function LayoutOverlayBase(incommingProps: LayoutOverlayBaseProps) {
|
|
|
88
99
|
const { scrollerRef, snap } = useScrollerContext()
|
|
89
100
|
const positions = useOverlayPosition()
|
|
90
101
|
const scrollTo = useScrollTo()
|
|
91
|
-
const [isPresent, safeToRemove] = usePresence()
|
|
92
102
|
const beforeRef = useRef<HTMLDivElement>(null)
|
|
93
103
|
|
|
94
|
-
const { closeSteps, active, direction } = usePageContext()
|
|
95
|
-
const close = useGo(closeSteps * -1)
|
|
96
|
-
|
|
97
104
|
const position = useMotionValue<OverlayPosition>(OverlayPosition.UNOPENED)
|
|
98
105
|
|
|
99
106
|
const classes = withState({ variantSm, variantMd, sizeSm, sizeMd, justifySm, justifyMd })
|
|
@@ -105,7 +112,15 @@ export function LayoutOverlayBase(incommingProps: LayoutOverlayBaseProps) {
|
|
|
105
112
|
// When the component is mounted, we need to set the initial position of the overlay.
|
|
106
113
|
useIsomorphicLayoutEffect(() => {
|
|
107
114
|
const scroller = scrollerRef.current
|
|
108
|
-
|
|
115
|
+
|
|
116
|
+
if (!scroller) return undefined
|
|
117
|
+
|
|
118
|
+
if (!isPresent && position.get() === OverlayPosition.UNOPENED) {
|
|
119
|
+
scroller.scrollLeft = positions.closed.x.get()
|
|
120
|
+
scroller.scrollTop = positions.closed.y.get()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!isPresent) return undefined
|
|
109
124
|
|
|
110
125
|
const open = { x: positions.open.x.get(), y: positions.open.y.get() }
|
|
111
126
|
|
|
@@ -148,7 +163,7 @@ export function LayoutOverlayBase(incommingProps: LayoutOverlayBaseProps) {
|
|
|
148
163
|
|
|
149
164
|
// When the overlay is closed by navigating away, we're closing the overlay.
|
|
150
165
|
useEffect(() => {
|
|
151
|
-
if (isPresent) return
|
|
166
|
+
if (isPresent || position.get() === OverlayPosition.UNOPENED) return
|
|
152
167
|
position.set(OverlayPosition.CLOSED)
|
|
153
168
|
clearScrollLock()
|
|
154
169
|
|
|
@@ -164,8 +179,8 @@ export function LayoutOverlayBase(incommingProps: LayoutOverlayBaseProps) {
|
|
|
164
179
|
if (position.get() !== OverlayPosition.OPENED) return
|
|
165
180
|
position.set(OverlayPosition.CLOSED)
|
|
166
181
|
clearScrollLock()
|
|
167
|
-
|
|
168
|
-
}, [
|
|
182
|
+
onClosed()
|
|
183
|
+
}, [onClosed, position])
|
|
169
184
|
|
|
170
185
|
// Handle escape key
|
|
171
186
|
const windowRef = useRef(typeof window !== 'undefined' ? window : null)
|
|
@@ -187,7 +202,6 @@ export function LayoutOverlayBase(incommingProps: LayoutOverlayBaseProps) {
|
|
|
187
202
|
}, [offsetY])
|
|
188
203
|
|
|
189
204
|
// Create the exact position for the LayoutProvider which offsets the top of the overlay
|
|
190
|
-
const offsetPageY = useScrollOffset().y
|
|
191
205
|
const scrollWithoffset = useTransform(
|
|
192
206
|
[scroll.y, positions.open.y, offsetY],
|
|
193
207
|
([y, openY, offsetYv]: number[]) => Math.max(0, y - openY - offsetYv + offsetPageY),
|
|
@@ -333,7 +347,6 @@ export function LayoutOverlayBase(incommingProps: LayoutOverlayBaseProps) {
|
|
|
333
347
|
marginTop: `calc(${smSpacingTop} * -1)`,
|
|
334
348
|
paddingTop: smSpacingTop,
|
|
335
349
|
},
|
|
336
|
-
|
|
337
350
|
'&.sizeSmFloating': {
|
|
338
351
|
padding: `${theme.page.vertical} ${theme.page.horizontal}`,
|
|
339
352
|
},
|
|
@@ -353,7 +366,8 @@ export function LayoutOverlayBase(incommingProps: LayoutOverlayBaseProps) {
|
|
|
353
366
|
},
|
|
354
367
|
})}
|
|
355
368
|
>
|
|
356
|
-
<
|
|
369
|
+
<MotionDiv
|
|
370
|
+
layout
|
|
357
371
|
className={classes.overlayPane}
|
|
358
372
|
sx={(theme) => ({
|
|
359
373
|
pointerEvents: 'all',
|
|
@@ -419,7 +433,7 @@ export function LayoutOverlayBase(incommingProps: LayoutOverlayBaseProps) {
|
|
|
419
433
|
})}
|
|
420
434
|
>
|
|
421
435
|
<LayoutProvider scroll={scrollWithoffset}>{children}</LayoutProvider>
|
|
422
|
-
</
|
|
436
|
+
</MotionDiv>
|
|
423
437
|
</Box>
|
|
424
438
|
</Scroller>
|
|
425
439
|
</>
|
|
File without changes
|
package/Overlay/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components'
|
|
@@ -108,11 +108,7 @@ export function DarkLightModeMenuSecondaryItem(props: ListItemButtonProps) {
|
|
|
108
108
|
<IconSvg src={currentMode === 'light' ? iconMoon : iconSun} size='medium' />
|
|
109
109
|
</ListItemIcon>
|
|
110
110
|
<ListItemText>
|
|
111
|
-
{currentMode === 'light' ?
|
|
112
|
-
<Trans id='Switch to Dark Mode' />
|
|
113
|
-
) : (
|
|
114
|
-
<Trans id='Switch to Light Mode' />
|
|
115
|
-
)}
|
|
111
|
+
{currentMode === 'light' ? <Trans id='Dark Mode' /> : <Trans id='Light Mode' />}
|
|
116
112
|
</ListItemText>
|
|
117
113
|
</ListItemButton>
|
|
118
114
|
)
|
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from './ActionCard/ActionCardList'
|
|
2
1
|
export * from './ActionCard/ActionCard'
|
|
2
|
+
export * from './ActionCard/ActionCardList'
|
|
3
3
|
export * from './AnimatedRow/AnimatedRow'
|
|
4
4
|
export * from './Blog/BlogAuthor/BlogAuthor'
|
|
5
5
|
export * from './Blog/BlogContent/BlogContent'
|
|
@@ -22,6 +22,7 @@ export * from './Form/InputCheckmark'
|
|
|
22
22
|
export * from './FramerScroller'
|
|
23
23
|
export * from './FullPageMessage/FullPageMessage'
|
|
24
24
|
export * from './Highlight/Highlight'
|
|
25
|
+
export * from './hooks'
|
|
25
26
|
export * from './IconHeader/IconHeader'
|
|
26
27
|
export * from './icons'
|
|
27
28
|
export * from './IconSvg'
|
|
@@ -30,6 +31,8 @@ export * from './Layout'
|
|
|
30
31
|
export * from './LayoutDefault'
|
|
31
32
|
export * from './LayoutOverlay'
|
|
32
33
|
export * from './LayoutParts'
|
|
34
|
+
export * from './Navigation'
|
|
35
|
+
export * from './Overlay'
|
|
33
36
|
export * from './Page'
|
|
34
37
|
export * from './PageLoadIndicator/PageLoadIndicator'
|
|
35
38
|
export * from './PageMeta/PageMeta'
|
|
@@ -39,8 +42,8 @@ export * from './Row'
|
|
|
39
42
|
export * from './SectionContainer/SectionContainer'
|
|
40
43
|
export * from './SectionHeader/SectionHeader'
|
|
41
44
|
export * from './Separator/Separator'
|
|
42
|
-
export * from './Snackbar/MessageSnackbar'
|
|
43
45
|
export * from './Snackbar/ErrorSnackbar'
|
|
46
|
+
export * from './Snackbar/MessageSnackbar'
|
|
44
47
|
export * from './Snackbar/MessageSnackbarImpl'
|
|
45
48
|
export * from './StarRatingField/StarRatingField'
|
|
46
49
|
export * from './Stepper/Stepper'
|
|
@@ -50,6 +53,5 @@ export * from './Theme'
|
|
|
50
53
|
export * from './TimeAgo/TimeAgo'
|
|
51
54
|
export * from './ToggleButton/ToggleButton'
|
|
52
55
|
export * from './ToggleButtonGroup/ToggleButtonGroup'
|
|
53
|
-
export * from './hooks'
|
|
54
56
|
export * from './UspList/UspList'
|
|
55
57
|
export * from './UspList/UspListItem'
|
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.
|
|
5
|
+
"version": "4.13.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.6.0",
|
|
22
22
|
"@graphcommerce/framer-next-pages": "3.2.3",
|
|
23
|
-
"@graphcommerce/framer-scroller": "2.1.
|
|
23
|
+
"@graphcommerce/framer-scroller": "2.1.22",
|
|
24
24
|
"@graphcommerce/framer-utils": "3.1.4",
|
|
25
25
|
"@graphcommerce/image": "3.1.7",
|
|
26
26
|
"react-is": "^18.1.0",
|