@graphcommerce/next-ui 4.12.0 → 4.14.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 +2 -2
- package/ActionCard/ActionCardList.tsx +107 -76
- package/ActionCard/ActionCardListForm.tsx +2 -1
- package/Blog/BlogListItem/BlogListItem.tsx +1 -1
- package/CHANGELOG.md +44 -0
- package/FramerScroller/SidebarGallery.tsx +12 -1
- package/Layout/components/LayoutHeader.tsx +1 -1
- package/Layout/components/LayoutHeaderContent.tsx +9 -4
- package/LayoutDefault/components/LayoutDefault.tsx +5 -3
- package/LayoutOverlay/components/LayoutOverlay.tsx +25 -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 +237 -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/PageMeta/PageMeta.tsx +4 -6
- package/Theme/DarkLightModeThemeProvider.tsx +1 -5
- package/index.ts +7 -3
- package/package.json +12 -10
- package/utils/cookie.ts +33 -0
|
@@ -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,237 @@
|
|
|
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
|
+
layout='position'
|
|
109
|
+
left={
|
|
110
|
+
showBack && (
|
|
111
|
+
<Fab
|
|
112
|
+
color='inherit'
|
|
113
|
+
onClick={handleOnBack}
|
|
114
|
+
sx={{
|
|
115
|
+
boxShadow: 'none',
|
|
116
|
+
marginLeft: `calc((${fabSize} - ${svgSize}) * -0.5)`,
|
|
117
|
+
marginRight: `calc((${fabSize} - ${svgSize}) * -0.5)`,
|
|
118
|
+
}}
|
|
119
|
+
size='responsive'
|
|
120
|
+
aria-label={i18n._(/* i18n */ 'Back')}
|
|
121
|
+
>
|
|
122
|
+
<IconSvg src={iconChevronLeft} size='large' aria-hidden />
|
|
123
|
+
</Fab>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
right={
|
|
127
|
+
<Fab
|
|
128
|
+
color='inherit'
|
|
129
|
+
onClick={() => onClose()}
|
|
130
|
+
sx={{
|
|
131
|
+
boxShadow: 'none',
|
|
132
|
+
marginLeft: `calc((${fabSize} - ${svgSize}) * -0.5)`,
|
|
133
|
+
marginRight: `calc((${fabSize} - ${svgSize}) * -0.5)`,
|
|
134
|
+
}}
|
|
135
|
+
size='responsive'
|
|
136
|
+
aria-label={i18n._(/* i18n */ 'Close')}
|
|
137
|
+
>
|
|
138
|
+
<IconSvg src={iconClose} size='large' aria-hidden />
|
|
139
|
+
</Fab>
|
|
140
|
+
}
|
|
141
|
+
>
|
|
142
|
+
<LayoutTitle size='small' component='span'>
|
|
143
|
+
{findCurrent(items, selected)?.name ?? <Trans id='Menu' />}
|
|
144
|
+
</LayoutTitle>
|
|
145
|
+
</LayoutHeaderContent>
|
|
146
|
+
</Box>
|
|
147
|
+
</MotionDiv>
|
|
148
|
+
|
|
149
|
+
<MotionDiv layout='position' style={{ display: 'grid' }}>
|
|
150
|
+
<Box
|
|
151
|
+
sx={(theme) => ({
|
|
152
|
+
display: 'grid',
|
|
153
|
+
alignItems: !stretchColumns ? 'start' : undefined,
|
|
154
|
+
|
|
155
|
+
[theme.breakpoints.down('md')]: {
|
|
156
|
+
overflow: 'hidden',
|
|
157
|
+
scrollSnapType: 'x mandatory',
|
|
158
|
+
width: `calc(${theme.spacings.md} + ${theme.spacings.md} + ${itemWidth})`,
|
|
159
|
+
},
|
|
160
|
+
'& .NavigationItem-item': {
|
|
161
|
+
width: itemWidth,
|
|
162
|
+
},
|
|
163
|
+
})}
|
|
164
|
+
>
|
|
165
|
+
<Box
|
|
166
|
+
className={classes.navigation}
|
|
167
|
+
sx={[
|
|
168
|
+
(theme) => ({
|
|
169
|
+
py: theme.spacings.md,
|
|
170
|
+
display: 'grid',
|
|
171
|
+
gridAutoFlow: 'column',
|
|
172
|
+
scrollSnapAlign: 'end',
|
|
173
|
+
'& > ul > li > a, & > ul > li > [role=button]': {
|
|
174
|
+
'& span': {
|
|
175
|
+
typography: 'h2',
|
|
176
|
+
},
|
|
177
|
+
// '& svg': { display: 'none' },
|
|
178
|
+
},
|
|
179
|
+
'& .Navigation-column': {},
|
|
180
|
+
'& .NavigationItem-item': {
|
|
181
|
+
mx: theme.spacings.md,
|
|
182
|
+
whiteSpace: 'nowrap',
|
|
183
|
+
},
|
|
184
|
+
'& .NavigationItem-item.first': {
|
|
185
|
+
// mt: theme.spacings.md,
|
|
186
|
+
},
|
|
187
|
+
'& .Navigation-column:first-of-type': {
|
|
188
|
+
boxShadow: 'none',
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
...(Array.isArray(sx) ? sx : [sx]),
|
|
192
|
+
]}
|
|
193
|
+
>
|
|
194
|
+
{selected.length >= 0 && (
|
|
195
|
+
<Box
|
|
196
|
+
sx={(theme) => ({
|
|
197
|
+
gridArea: '1 / 1 / 999 / 2',
|
|
198
|
+
boxShadow: `inset 1px 0 ${theme.palette.divider}`,
|
|
199
|
+
})}
|
|
200
|
+
className={classes.column}
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
{selected.length >= 1 && (
|
|
204
|
+
<Box
|
|
205
|
+
sx={(theme) => ({
|
|
206
|
+
gridArea: '1 / 2 / 999 / 3',
|
|
207
|
+
boxShadow: `inset 1px 0 ${theme.palette.divider}`,
|
|
208
|
+
})}
|
|
209
|
+
className={classes.column}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
{selected.length >= 2 && (
|
|
213
|
+
<Box
|
|
214
|
+
sx={(theme) => ({
|
|
215
|
+
gridArea: '1 / 3 / 999 / 4',
|
|
216
|
+
boxShadow: `inset 1px 0 ${theme.palette.divider}`,
|
|
217
|
+
})}
|
|
218
|
+
className={classes.column}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
{selected.length >= 3 && (
|
|
222
|
+
<Box
|
|
223
|
+
sx={(theme) => ({
|
|
224
|
+
gridArea: '1 / 4 / 999 / 5',
|
|
225
|
+
boxShadow: `inset 1px 0 ${theme.palette.divider}`,
|
|
226
|
+
})}
|
|
227
|
+
className={classes.column}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
<NavigationList items={items} selected />
|
|
232
|
+
</Box>
|
|
233
|
+
</Box>
|
|
234
|
+
</MotionDiv>
|
|
235
|
+
</Overlay>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
@@ -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
|
+
}
|