Skip to content
Closed
6 changes: 6 additions & 0 deletions .changeset/kind-days-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphcommerce/magento-cart': patch
'@graphcommerce/next-ui': patch
---

Added disableScrollEffects prop to CartFab & NavigationFab for easier customization of the header
10 changes: 10 additions & 0 deletions .changeset/red-rooms-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@graphcommerce/magento-open-source': minor
'@graphcommerce/magento-storyblok': minor
'@graphcommerce/magento-graphcms': minor
'@graphcommerce/next-ui': minor
---

Refactored `LayoutNavigation` into composable pieces (`Header`, `HeaderContainer`, `MenuOverlay`, project-local `LayoutDefault`).
The old version is preserved as `LayoutNavigationLegacy.tsx` and swappable via a one-line edit in `components/Layout/index.ts`.
`LayoutDefault` / `LayoutDefaultProps` in `@graphcommerce/next-ui` are marked `@deprecated` — the canonical version now lives locally in `components/Layout/`.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Footer'
82 changes: 82 additions & 0 deletions examples/magento-graphcms/components/Layout/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useCartEnabled } from '@graphcommerce/magento-cart'
import { CustomerFab } from '@graphcommerce/magento-customer'
import { SearchFab, SearchField } from '@graphcommerce/magento-search'
import { StoreSwitcherButton, StoreSwitcherFab } from '@graphcommerce/magento-store'
import { WishlistFab } from '@graphcommerce/magento-wishlist'
import {
DesktopNavActions,
DesktopNavBar,
DesktopNavItem,
iconChevronDown,
iconCustomerService,
iconHeart,
IconSvg,
MobileTopRight,
PlaceholderFab,
type UseNavigationSelection,
} from '@graphcommerce/next-ui'
import { t } from '@lingui/core/macro'
import { Trans } from '@lingui/react/macro'
import { Fab } from '@mui/material'
import { productListRenderer } from '../../ProductListItems/productListRenderer'
import { HeaderContainer } from './HeaderContainer'
import type { LayoutQuery } from '../Layout.gql'
import { Logo } from '../Logo'

export type HeaderProps = LayoutQuery & { selection: UseNavigationSelection }

export function Header(props: HeaderProps) {
const { menu, selection } = props
const cartEnabled = useCartEnabled()

return (
<HeaderContainer>
<Logo />

<DesktopNavBar>
{menu?.items?.[0]?.children?.slice(0, 2).map((item) => (
<DesktopNavItem key={item?.uid} href={`/${item?.url_path}`}>
{item?.name}
</DesktopNavItem>
))}

<DesktopNavItem
onClick={() => selection.set([menu?.items?.[0]?.uid || ''])}
onKeyUp={(evt) => {
if (evt.key === 'Enter') {
selection.set([menu?.items?.[0]?.uid || ''])
}
}}
tabIndex={0}
>
{menu?.items?.[0]?.name}
<IconSvg src={iconChevronDown} />
</DesktopNavItem>

<DesktopNavItem href='/blog'>
<Trans>Blog</Trans>
</DesktopNavItem>
</DesktopNavBar>

<DesktopNavActions>
<SearchField
formControl={{ sx: { width: '400px' } }}
searchField={{ productListRenderer }}
/>
<StoreSwitcherButton />
<Fab href='/service' aria-label={t`Customer Service`} size='large' color='inherit'>
<IconSvg src={iconCustomerService} size='large' />
</Fab>
<WishlistFab icon={<IconSvg src={iconHeart} size='large' />} />
<CustomerFab guestHref='/account/signin' authHref='/account' />
{/* The placeholder exists because the CartFab is sticky but we want to reserve the space for the <CartFab /> */}
{cartEnabled && <PlaceholderFab />}
</DesktopNavActions>

<MobileTopRight>
<StoreSwitcherFab size='responsiveMedium' />
<SearchFab size='responsiveMedium' />
</MobileTopRight>
</HeaderContainer>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ContainerSizingProps } from '@graphcommerce/next-ui'
import { Container, sxx } from '@graphcommerce/next-ui'

export type HeaderContainerProps = ContainerSizingProps

export function HeaderContainer(props: HeaderContainerProps) {
const { children, sx, ...containerProps } = props

return (
<Container
sizing='shell'
maxWidth={false}
component='header'
{...containerProps}
sx={sxx(
(theme) => ({
zIndex: theme.zIndex.appBar - 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: theme.appShell.headerHeightSm,
pointerEvents: 'none',
'& > *': {
pointerEvents: 'all',
},
[theme.breakpoints.up('md')]: {
height: theme.appShell.headerHeightMd,
top: 0,
display: 'flex',
justifyContent: 'left',
width: '100%',
},
}),
sx,
)}
>
{children}
</Container>
)
}
2 changes: 2 additions & 0 deletions examples/magento-graphcms/components/Layout/Header/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Header'
export * from './HeaderContainer'
134 changes: 134 additions & 0 deletions examples/magento-graphcms/components/Layout/LayoutDefault.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useScrollOffset } from '@graphcommerce/framer-next-pages'
import { dvh } from '@graphcommerce/framer-utils'
import {
Container,
extendableComponent,
LayoutProvider,
SkipLink,
sxx,
useFabSize,
} from '@graphcommerce/next-ui'
import type { SxProps, Theme } from '@mui/material'
import { Box } from '@mui/material'
import { useScroll, useTransform } from 'framer-motion'

export type LayoutDefaultProps = {
className?: string
beforeHeader?: React.ReactNode
header: React.ReactNode
footer: React.ReactNode
menuFab?: React.ReactNode
cartFab?: React.ReactNode
children?: React.ReactNode
noSticky?: boolean
sx?: SxProps<Theme>
} & OwnerState

type OwnerState = {
noSticky?: boolean
}
const parts = ['root', 'fabs', 'header', 'children', 'footer'] as const
const { withState } = extendableComponent<OwnerState, 'LayoutDefault', typeof parts>(
'LayoutDefault',
parts,
)

export function LayoutDefault(props: LayoutDefaultProps) {
const {
children,
header,
beforeHeader,
footer,
menuFab,
cartFab,
noSticky,
className,
sx = [],
} = props

const { scrollY } = useScroll()
const scrollYOffset = useTransform(
[scrollY, useScrollOffset()],
([y, offset]: number[]) => y + offset,
)

const classes = withState({ noSticky })
const fabIconSize = useFabSize('responsive')

return (
<Box
className={`${classes.root} ${className ?? ''}`}
sx={sxx(
(theme) => ({
minHeight: dvh(100),
'@supports (-webkit-touch-callout: none)': {
minHeight: '-webkit-fill-available',
},
display: 'grid',
gridTemplateRows: { xs: 'auto 1fr auto', md: 'auto auto 1fr auto' },
gridTemplateColumns: '100%',
background: theme.vars.palette.background.default,
}),
sx,
)}
>
<SkipLink />
<LayoutProvider scroll={scrollYOffset}>
{beforeHeader}
{header}
{menuFab || cartFab ? (
<Container
sizing='shell'
maxWidth={false}
className={classes.fabs}
sx={(theme) => ({
display: 'flex',
justifyContent: 'space-between',
width: '100%',
height: 0,
zIndex: 'speedDial',
[theme.breakpoints.up('sm')]: {
position: 'sticky',
marginTop: `calc(${theme.appShell.headerHeightMd} * -1 - calc(${fabIconSize} / 2))`,
top: `calc(${theme.appShell.headerHeightMd} / 2 - (${fabIconSize} / 2))`,
},
[theme.breakpoints.down('md')]: {
position: 'fixed',
top: 'unset',
bottom: `calc(20px + ${fabIconSize})`,
padding: '0 20px',
'@media (max-height: 530px) and (orientation: portrait)': {
display: 'none',
},
},
})}
>
{menuFab}
{cartFab && (
<Box
sx={(theme) => ({
display: 'flex',
flexDirection: 'row-reverse',
gap: theme.spacings.sm,
[theme.breakpoints.up('md')]: {
flexDirection: 'column',
alignItems: 'flex-end',
},
})}
>
{cartFab}
</Box>
)}
</Container>
) : (
<div />
)}
<div className={classes.children}>
<div id='skip-nav' tabIndex={-1} />
{children}
</div>
<div className={classes.footer}>{footer}</div>
</LayoutProvider>
</Box>
)
}
9 changes: 7 additions & 2 deletions examples/magento-graphcms/components/Layout/LayoutMinimal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LayoutDefault, LayoutDefaultProps } from '@graphcommerce/next-ui'
import { Footer } from './Footer'
import { HeaderContainer } from './Header'
import { LayoutQuery } from './Layout.gql'
import { LayoutDefault, type LayoutDefaultProps } from './LayoutDefault'
import { Logo } from './Logo'

export type LayoutMinimalProps = LayoutQuery &
Expand All @@ -12,7 +13,11 @@ export function LayoutMinimal(props: LayoutMinimalProps) {
return (
<LayoutDefault
{...uiProps}
header={<Logo />}
header={
<HeaderContainer>
<Logo />
</HeaderContainer>
}
footer={<Footer footer={footer} />}
sx={(theme) => ({ background: theme.vars.palette.background.paper })}
>
Expand Down
Loading
Loading