Skip to content

Commit

Permalink
Fix mobile sidebar and switch from Headless UI to Radix UI
Browse files Browse the repository at this point in the history
  • Loading branch information
danielweinmann committed Jan 20, 2025
1 parent bd4f09d commit c91c522
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 415 deletions.
4 changes: 2 additions & 2 deletions apps/web/app/routes/conf/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Link, Outlet } from 'react-router'
import ExternalLink from '~/ui/external-link'
import SidebarLayout from '~/ui/sidebar-layout'
import { SidebarLayout } from '~/ui/sidebar-layout'
import SecondaryButtonLink from '~/ui/secondary-button-link'
import TopBar from '~/ui/conf/top-bar'
import { Route } from './+types/layout'
Expand All @@ -15,7 +15,7 @@ export default function Component({ matches }: Route.ComponentProps) {
return (
<div className="relative isolate flex grow flex-col">
<SidebarLayout>
<SidebarLayout.Nav>
<SidebarLayout.Nav menuTitle="View steps">
<SidebarLayout.NavTitle>From scratch</SidebarLayout.NavTitle>
<SidebarLayout.NavLink to={'/conf/01'}>
01. Quick and dirty
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/routes/examples/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { kebabCase } from 'lodash-es'
import { Fragment } from 'react'
import { Outlet } from 'react-router'
import { exampleRouteGroups } from '~/routes'
import SidebarLayout from '~/ui/sidebar-layout'
import { SidebarLayout } from '~/ui/sidebar-layout'

export default function Component() {
return (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/ui/base-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function BaseButton({
return (
<button
className={cx(
'inline-flex items-center justify-center rounded-md border px-6 py-2 text-base font-medium shadow-sm ring-2 ring-transparent ring-offset-2 ring-offset-transparent focus:outline-none disabled:bg-gray-400',
'inline-flex items-center justify-center rounded-md border px-6 py-2 text-base font-medium shadow-sm ring-2 ring-transparent ring-offset-2 ring-offset-transparent focus:outline-none disabled:bg-gray-400 gap-1',
className,
)}
{...props}
Expand Down
207 changes: 58 additions & 149 deletions apps/web/app/ui/sidebar-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,179 +1,88 @@
import * as React from 'react'
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
Popover,
PopoverButton,
PopoverPanel,
} from '@headlessui/react'
import { ChevronDownIcon, XMarkIcon } from '@heroicons/react/24/outline'
import * as Collapsible from '@radix-ui/react-collapsible'
import { useEffect, useState, type ReactNode } from 'react'
import { useLocation, type NavLinkProps } from 'react-router'
import { cx } from '~/helpers'
import {
Bars3BottomLeftIcon,
Bars3BottomRightIcon,
XMarkIcon,
} from '@heroicons/react/24/outline'
import UINavLink from '~/ui/nav-link'
import type { NavLinkProps } from 'react-router'
import SecondaryButton from './secondary-button'

type SidebarType = 'disclosure' | 'popover'

type NavProps = {
type?: SidebarType
close?: Function
} & Omit<JSX.IntrinsicElements['nav'], 'ref'>
function Nav({
children,
menuTitle = 'More examples',
}: {
children: ReactNode
menuTitle?: string
}) {
const [open, setOpen] = useState(false)
const location = useLocation()

function Nav({ children, type = 'disclosure', close, ...props }: NavProps) {
const Panel = type === 'disclosure' ? DisclosurePanel : PopoverPanel
const Button = type === 'disclosure' ? DisclosureButton : PopoverButton
const Icon = type === 'disclosure' ? Bars3BottomRightIcon : XMarkIcon
const classes = type === 'disclosure' ? 'min-h-full' : 'absolute top-0'
useEffect(() => {
setOpen(false)
}, [location])

return (
<Panel as="nav" {...props}>
<div className={cx('z-10 w-[14rem] bg-pink-600 p-2 pb-4', classes)}>
<div className="flex justify-end p-1">
<Button className="inline-flex items-center justify-center rounded-md p-1 text-[#480803] hover:bg-pink-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<Icon className="block h-6 w-6" />
</Button>
<Collapsible.Root open={open} onOpenChange={setOpen} asChild>
<nav className="relative">
<Collapsible.Trigger asChild>
<div className="md:hidden px-4 flex w-full">
{open ? (
<button className="absolute top-1 right-1 inline-flex items-center justify-center rounded-md p-2 text-pink-900 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<XMarkIcon className="block h-6 w-6" aria-hidden="true" />
</button>
) : (
<SecondaryButton className="flex-1 mt-2">
<span>{menuTitle}</span>
<ChevronDownIcon className="size-6" aria-hidden="true" />
</SecondaryButton>
)}
</div>
</Collapsible.Trigger>
<Collapsible.Content asChild>
<div className="block md:hidden rounded bg-pink-600 p-2">
<div className="flex flex-col gap-2 pb-2">{children}</div>
</div>
</Collapsible.Content>
<div className="md:block hidden w-[14rem] bg-pink-600 p-2 min-h-full">
<div className="flex flex-col gap-2 pb-2">{children}</div>
</div>
<div className="-mt-4 flex flex-col space-y-2">
{React.Children.map(children, (child) => {
if (!React.isValidElement(child)) return child

if (child.type === NavLink) {
return React.cloneElement(child, {
type,
close,
} as React.HTMLAttributes<unknown>)
}

return child
})}
</div>
</div>
</Panel>
</nav>
</Collapsible.Root>
)
}

function NavTitle({ className, ...props }: JSX.IntrinsicElements['h4']) {
return (
<h4 className={cx('font-medium text-[#480803]', className)} {...props} />
)
function NavTitle({ children }: { children: ReactNode }) {
return <h4 className="font-medium text-[#480803]">{children}</h4>
}

function NavLink({
className,
type = 'disclosure',
close = () => {},
...props
}: NavLinkProps & {
type?: SidebarType
close?: Function
}) {
function NavLink({ className, ...props }: NavLinkProps) {
return (
<UINavLink
className={({ isActive }) =>
className={({ isActive, isPending, isTransitioning }) =>
cx(
isActive ? 'bg-pink-900' : 'hover:bg-pink-700',
typeof className === 'function'
? className({ isActive, isPending: false, isTransitioning: false })
? className({ isActive, isPending, isTransitioning })
: className,
)
}
onClick={type === 'popover' ? () => close() : undefined}
{...props}
/>
)
}

type ContentProps = { type?: SidebarType } & JSX.IntrinsicElements['div']

function Content({ children, type, className, ...props }: ContentProps) {
return (
<div
className={cx(type === 'disclosure' ? 'flex-1' : 'pl-10', className)}
{...props}
>
{children}
</div>
)
}

function Closed({ type }: { type: SidebarType }) {
const Button = type === 'disclosure' ? DisclosureButton : PopoverButton

return (
<div className={cx('absolute inset-y-0 w-10 bg-pink-600 p-1 md:relative')}>
<div className="relative h-full">
<Button className="sticky top-0 inline-flex items-center justify-center rounded-md p-1 text-[#480803] hover:bg-pink-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<Bars3BottomLeftIcon className="block h-6 w-6" />
</Button>
</div>
</div>
)
}

type MapChildren = {
children: React.ReactNode
type: SidebarType
open: boolean
close: Function
}

function mapChildren({ children, type, close }: MapChildren) {
return React.Children.map(children, (child) => {
if (!React.isValidElement(child)) return child

if (child.type === Nav) {
return React.cloneElement(child, {
type,
close,
} as React.HTMLAttributes<unknown>)
}

if (child.type === Content) {
return React.cloneElement(child, {
type,
} as React.HTMLAttributes<unknown>)
}

return child
})
function Content({ children }: { children: ReactNode }) {
return <div className="flex-1">{children}</div>
}

function SidebarRoot({
children,
className,
...props
}: Omit<JSX.IntrinsicElements['div'], 'ref'>) {
return (
<>
<Disclosure
as="div"
defaultOpen
className={cx('hidden grow md:flex', className)}
{...props}
>
{({ open, close }) => (
<>
{!open && <Closed type="disclosure" />}
{mapChildren({ children, type: 'disclosure', open, close })}
</>
)}
</Disclosure>
<Popover as="div" className={cx('md:hidden', className)} {...props}>
{({ open, close }) => (
<>
{!open && <Closed type="popover" />}
{mapChildren({ children, type: 'popover', open, close })}
</>
)}
</Popover>
</>
)
function SidebarRoot({ children }: { children: ReactNode }) {
return <div className="flex md:flex-row flex-col p-2 md:p-0">{children}</div>
}

const Sidebar = Object.assign(SidebarRoot, { Nav, NavTitle, NavLink, Content })
const SidebarLayout = Object.assign(SidebarRoot, {
Nav,
NavTitle,
NavLink,
Content,
})

export default Sidebar
export { SidebarLayout }
Loading

0 comments on commit c91c522

Please sign in to comment.