Skip to content

Commit

Permalink
Merge pull request #21 from thomasKn/thomas/fv-227-mobile-header-menu
Browse files Browse the repository at this point in the history
Add mobile nav
  • Loading branch information
thomasKn authored Feb 2, 2024
2 parents eca4b30 + f9c8ec0 commit b885b84
Show file tree
Hide file tree
Showing 14 changed files with 341 additions and 74 deletions.
9 changes: 1 addition & 8 deletions app/components/icons/IconClose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,8 @@ export function IconClose(props: IconProps) {
return (
<Icon {...props} stroke={props.stroke || 'currentColor'}>
<title>Close</title>
<line x1="4.44194" x2="15.7556" y1="4.30806" y2="15.6218" />
<line
strokeWidth="1.25"
x1="4.44194"
x2="15.7556"
y1="4.30806"
y2="15.6218"
/>
<line
strokeWidth="1.25"
transform="matrix(-0.707107 0.707107 0.707107 0.707107 16 4.75)"
x2="16"
y1="-0.625"
Expand Down
14 changes: 14 additions & 0 deletions app/components/icons/IconMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {IconProps} from './Icon';

import {Icon} from './Icon';

export function IconMenu(props: IconProps) {
return (
<Icon {...props} stroke={props.stroke || 'currentColor'}>
<title>Menu</title>
<line strokeWidth="1.25" x1="3" x2="17" y1="6.375" y2="6.375" />
<line strokeWidth="1.25" x1="3" x2="17" y1="10.375" y2="10.375" />
<line strokeWidth="1.25" x1="3" x2="17" y1="14.375" y2="14.375" />
</Icon>
);
}
5 changes: 4 additions & 1 deletion app/components/layout/CartCount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ function Badge(props: {cart?: CartType | null; count: number}) {
[count, isHydrated],
);

const buttonClass = cx(['relative flex size-8 items-center justify-center']);
const buttonClass = cx([
'relative flex size-8 items-center justify-center',
count > 0 && 'mr-3 md:mr-0',
]);

return isHydrated ? (
<Sheet onOpenChange={setCartOpen} open={cartOpen}>
Expand Down
54 changes: 28 additions & 26 deletions app/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import type {CSSProperties} from 'react';
import {Link} from '@remix-run/react';
import {vercelStegaCleanAll} from '@sanity/client/stega';
import {cx} from 'class-variance-authority';
import {LazyMotion, m, useTransform} from 'framer-motion';
import {LazyMotion, m} from 'framer-motion';
import React from 'react';

import {useBoundedScroll} from '~/hooks/useBoundedScroll';
import {useLocalePath} from '~/hooks/useLocalePath';
import {useSanityRoot} from '~/hooks/useSanityRoot';
import {useScrollDirection} from '~/hooks/useScrollDirection';
import {useSettingsCssVars} from '~/hooks/useSettingsCssVars';

import {headerVariants} from '../cva/header';
import {Navigation} from '../navigation/Navigation';
import {DesktopNavigation} from '../navigation/DesktopNavigation';
import {MobileNavigation} from '../navigation/MobileNavigation';
import {CartCount} from './CartCount';
import {Logo} from './Logo';

Expand All @@ -30,11 +32,18 @@ export function Header() {
return (
<HeaderWrapper>
<style dangerouslySetInnerHTML={{__html: cssVars}} />
<div className="container">
<div
className="md:container"
style={
{
'--mobileHeaderXPadding': '0.75rem',
} as React.CSSProperties
}
>
<div className="flex items-center justify-between">
<Link prefetch="intent" to={homePath}>
<Logo
className="h-auto w-[var(--logoWidth)]"
className="h-auto w-[var(--logoWidth)] pl-[var(--mobileHeaderXPadding)] md:pl-0"
sizes={logoWidth}
style={
{
Expand All @@ -43,12 +52,10 @@ export function Header() {
}
/>
</Link>
<div className="flex items-center gap-3">
{/* Desktop navigation */}
<div className="hidden md:block">
<Navigation data={header?.menu} />
</div>
<div className="flex items-center gap-0 md:gap-2">
<DesktopNavigation data={header?.menu} />
<CartCount />
<MobileNavigation data={header?.menu} />
</div>
</div>
</div>
Expand Down Expand Up @@ -86,27 +93,22 @@ function HeaderAnimation(props: {
children: React.ReactNode;
className: string;
}) {
// See Animated Sticky Header from Build UI (https://buildui.com/recipes/fixed-header)
const loadFeatures = async () =>
await import('../../lib/framerMotionFeatures').then((res) => res.default);
const {scrollYBoundedProgress} = useBoundedScroll(400);
const scrollYBoundedProgressDelayed = useTransform(
scrollYBoundedProgress,
[0, 0.75, 1],
[0, 0, 1],
);

const style = {
transform: useTransform(
scrollYBoundedProgressDelayed,
[0, 1],
['translateY(0)', 'translateY(-100%)'],
),
};
const {direction} = useScrollDirection();

return (
<LazyMotion features={loadFeatures} strict>
<m.header className={props.className} style={style}>
<m.header
animate={{
y: direction === 'up' || !direction ? 0 : '-100%',
}}
className={props.className}
transition={{
duration: 0.1,
ease: 'linear',
}}
>
{props.children}
</m.header>
</LazyMotion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import {
import {NestedNavigation} from './NestedNavigation';

type HeaderQuery = InferType<typeof HEADER_QUERY>;
type NavigationProps = NonNullable<HeaderQuery>['menu'];
export type NavigationProps = NonNullable<HeaderQuery>['menu'];

export function Navigation(props: {data?: NavigationProps}) {
export function DesktopNavigation(props: {data?: NavigationProps}) {
const menuRef = useRef<HTMLUListElement>(null);
const [activeItem, setActiveItem] = useState<null | string | undefined>(null);
const dropdownWidth = 200;
Expand All @@ -28,7 +28,7 @@ export function Navigation(props: {data?: NavigationProps}) {
);

return (
<NavigationMenu id="header-nav">
<NavigationMenu className="hidden md:block" id="header-nav">
<CssVars
dropdownWidth={dropdownWidth}
viewportPosition={viewportPosition}
Expand Down
122 changes: 122 additions & 0 deletions app/components/navigation/MobileNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {useCallback, useState} from 'react';

import type {NavigationProps} from './DesktopNavigation';
import type {SanityNestedNavigationProps} from './NestedNavigation';

import {IconMenu} from '../icons/IconMenu';
import {SanityExternalLink} from '../sanity/link/SanityExternalLink';
import {SanityInternalLink} from '../sanity/link/SanityInternalLink';
import {
MobileNavigationMenu,
MobileNavigationMenuContent,
MobileNavigationMenuItem,
MobileNavigationMenuLink,
MobileNavigationMenuList,
MobileNavigationMenuTrigger,
} from '../ui/MobileNavigationMenu';
import {Sheet, SheetContent, SheetTrigger} from '../ui/Sheet';

export function MobileNavigation(props: {data?: NavigationProps}) {
const [open, setOpen] = useState(false);
const handleClose = useCallback(() => setOpen(false), []);

if (!props.data) return null;

// Todo => Add <Navlink /> support
return (
<div className="md:hidden">
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger className="flex items-center justify-center p-2 pr-[var(--mobileHeaderXPadding)] md:pr-0">
<IconMenu className="size-7" strokeWidth={1.5} />
</SheetTrigger>
<SheetContent
className="flex max-h-screen min-h-full w-screen flex-col gap-0 bg-background p-0 text-foreground sm:max-w-lg"
onCloseAutoFocus={(e) => e.preventDefault()}
onOpenAutoFocus={(e) => e.preventDefault()}
>
<MobileNavigationMenu className="flex-1 overflow-y-scroll text-xl font-medium">
<MobileNavigationMenuList>
{props.data &&
props.data?.length > 0 &&
props.data?.map((item) => (
<MobileNavigationMenuItem key={item._key}>
{item._type === 'internalLink' && (
<MobileNavigationMenuLink asChild onClick={handleClose}>
<div>
<SanityInternalLink data={item} />
</div>
</MobileNavigationMenuLink>
)}
{item._type === 'externalLink' && (
<MobileNavigationMenuLink asChild onClick={handleClose}>
<div>
<SanityExternalLink data={item} />
</div>
</MobileNavigationMenuLink>
)}
{item._type === 'nestedNavigation' && (
<NestedMobileNavigation
data={item}
onClose={handleClose}
/>
)}
</MobileNavigationMenuItem>
))}
</MobileNavigationMenuList>
</MobileNavigationMenu>
</SheetContent>
</Sheet>
</div>
);
}

function NestedMobileNavigation(props: {
data?: SanityNestedNavigationProps;
onClose: () => void;
}) {
const {data} = props;

if (!data) return null;

const {childLinks} = data;

return data.name && childLinks && childLinks.length > 0 ? (
<>
<MobileNavigationMenuTrigger>{data.name}</MobileNavigationMenuTrigger>
<MobileNavigationMenuContent className="flex-1 overflow-y-scroll">
<MobileNavigationMenuList>
{childLinks &&
childLinks.length > 0 &&
childLinks.map((child) => (
<MobileNavigationMenuLink
asChild
key={child._key}
onClick={props.onClose}
>
<div>
{child._type === 'internalLink' ? (
<SanityInternalLink data={child} />
) : child._type === 'externalLink' ? (
<SanityExternalLink data={child} />
) : null}
</div>
</MobileNavigationMenuLink>
))}
</MobileNavigationMenuList>
</MobileNavigationMenuContent>
</>
) : data.link && data.name && (!childLinks || childLinks.length === 0) ? (
// Render internal link if no child links
<SanityInternalLink
data={{
_key: data._key,
_type: 'internalLink',
anchor: null,
link: data.link,
name: data.name,
}}
>
{data.name}
</SanityInternalLink>
) : null;
}
2 changes: 1 addition & 1 deletion app/components/navigation/NestedNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
navigationMenuTriggerStyle,
} from '../ui/NavigationMenu';

type SanityNestedNavigationProps = TypeFromSelection<
export type SanityNestedNavigationProps = TypeFromSelection<
typeof NESTED_NAVIGATION_FRAGMENT
>;

Expand Down
Loading

0 comments on commit b885b84

Please sign in to comment.