فهرست منبع

feat: make sidebar responsive and accessible

satnaing 2 سال پیش
والد
کامیت
48208bb1c0
2فایلهای تغییر یافته به همراه57 افزوده شده و 25 حذف شده
  1. 24 14
      src/components/nav.tsx
  2. 33 11
      src/components/sidebar.tsx

+ 24 - 14
src/components/nav.tsx

@@ -1,5 +1,10 @@
 import { Link } from 'react-router-dom'
-import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from './ui/tooltip'
 import { cn } from '@/lib/utils'
 import { Button, buttonVariants } from './ui/button'
 import {
@@ -20,12 +25,12 @@ import {
 } from '@/components/ui/dropdown-menu'
 import useCheckActiveNav from '@/hooks/useCheckActiveNav'
 
-interface NavProps {
+interface NavProps extends React.HTMLAttributes<HTMLDivElement> {
   isCollapsed: boolean
   links: SideLink[]
 }
 
-export default function Nav({ links, isCollapsed }: NavProps) {
+export default function Nav({ links, isCollapsed, className }: NavProps) {
   const renderLink = ({ sub, ...rest }: SideLink) => {
     const key = `${rest.title}-${rest.href}`
     if (isCollapsed && sub)
@@ -40,11 +45,16 @@ export default function Nav({ links, isCollapsed }: NavProps) {
   return (
     <div
       data-collapsed={isCollapsed}
-      className='group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2'
+      className={cn(
+        'group flex flex-col gap-4 overflow-hidden border-b  py-2 transition-[max-height,padding] duration-500 data-[collapsed=true]:py-2 md:border-none',
+        className
+      )}
     >
-      <nav className='grid gap-1 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2'>
-        {links.map(renderLink)}
-      </nav>
+      <TooltipProvider delayDuration={0}>
+        <nav className='grid gap-1 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2'>
+          {links.map(renderLink)}
+        </nav>
+      </TooltipProvider>
     </div>
   )
 }
@@ -63,14 +73,14 @@ function NavLink({ title, icon, label, href, subLink = false }: NavLinkProps) {
           variant: checkActiveNav(href) ? 'secondary' : 'ghost',
           size: 'sm',
         }),
-        'justify-start px-6 h-12 rounded-none',
-        subLink && 'border-l border-l-slate-500 w-full h-10'
+        'h-12 justify-start rounded-none px-6',
+        subLink && 'h-10 w-full border-l border-l-slate-500'
       )}
     >
       <div className='mr-2'>{icon}</div>
       {title}
       {label && (
-        <div className='ml-2 bg-primary text-primary-foreground px-1 rounded-lg text-[0.625rem]'>
+        <div className='ml-2 rounded-lg bg-primary px-1 text-[0.625rem] text-primary-foreground'>
           {label}
         </div>
       )}
@@ -90,13 +100,13 @@ function NavLinkDropdown({ title, icon, label, sub }: SideLink) {
       <CollapsibleTrigger
         className={cn(
           buttonVariants({ variant: 'ghost', size: 'sm' }),
-          'justify-start px-6 h-12 rounded-none w-full group'
+          'group h-12 w-full justify-start rounded-none px-6'
         )}
       >
         <div className='mr-2'>{icon}</div>
         {title}
         {label && (
-          <div className='ml-2 bg-primary text-primary-foreground px-1 rounded-lg text-[0.625rem]'>
+          <div className='ml-2 rounded-lg bg-primary px-1 text-[0.625rem] text-primary-foreground'>
             {label}
           </div>
         )}
@@ -111,7 +121,7 @@ function NavLinkDropdown({ title, icon, label, sub }: SideLink) {
       <CollapsibleContent className='collapsibleDropdown' asChild>
         <ul>
           {sub!.map((sublink) => (
-            <li key={sublink.title} className='ml-8 my-1'>
+            <li key={sublink.title} className='my-1 ml-8'>
               <NavLink {...sublink} subLink />
             </li>
           ))}
@@ -165,7 +175,7 @@ function NavLinkIconDropdown({ title, icon, label, sub }: SideLink) {
             <Button
               variant={isChildActive ? 'secondary' : 'ghost'}
               size='icon'
-              className='w-12 h-12'
+              className='h-12 w-12'
             >
               {icon}
             </Button>

+ 33 - 11
src/components/sidebar.tsx

@@ -1,10 +1,10 @@
 import Nav from './nav'
-import { TooltipProvider } from './ui/tooltip'
 import { cn } from '@/lib/utils'
 import { Separator } from './ui/separator'
 import { Button } from './ui/button'
 import { sidelinks } from '@/data/sidelinks'
-import { IconChevronsLeft } from '@tabler/icons-react'
+import { IconChevronsLeft, IconMenu2, IconX } from '@tabler/icons-react'
+import { useState } from 'react'
 
 interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
   isCollapsed: boolean
@@ -16,15 +16,18 @@ export default function Sidebar({
   isCollapsed,
   setIsCollapsed,
 }: SidebarProps) {
+  const [navOpened, setNavOpened] = useState(false)
   return (
     <div
       className={cn(
-        'pb-12 relative hidden lg:block border-r-2 border-r-slate-100 dark:border-r-slate-900 transition-[width]',
+        `relative w-full border-r-2 border-r-slate-100 transition-[width] dark:border-r-slate-900 md:pb-12 ${isCollapsed ? 'md:w-14' : 'md:w-64'}`,
         className
       )}
     >
-      <TooltipProvider delayDuration={0}>
-        <div className='px-4 py-2 flex gap-2 h-16 items-center'>
+      <div
+        className={`flex h-16 w-full items-center justify-between gap-2 px-4 py-2`}
+      >
+        <div className='flex items-center gap-2'>
           <svg
             xmlns='http://www.w3.org/2000/svg'
             viewBox='0 0 256 256'
@@ -56,24 +59,43 @@ export default function Sidebar({
             <span className='sr-only'>Website Name</span>
           </svg>
           <div
-            className={`flex flex-col justify-end truncate ${isCollapsed ? 'w-0 invisible' : 'w-auto visible'}`}
+            className={`flex flex-col justify-end truncate ${isCollapsed ? 'invisible w-0' : 'visible w-auto'}`}
           >
             <span className='font-medium'>Shadcn Dashboard</span>
             <span className='text-xs'>Vite + ShadcnUI</span>
           </div>
         </div>
-        <Separator className='mb-4' />
-        <Nav isCollapsed={isCollapsed} links={sidelinks} />
-      </TooltipProvider>
+        <Button
+          variant='ghost'
+          size='icon'
+          className='md:hidden'
+          aria-label='Toggle Navigation'
+          aria-controls='sidebar-menu'
+          aria-expanded={navOpened}
+          onClick={() => setNavOpened((prev) => !prev)}
+        >
+          {navOpened ? <IconX /> : <IconMenu2 />}
+        </Button>
+      </div>
+
+      <Separator className='mb-4 hidden md:block' />
+      <Nav
+        id='sidebar-menu'
+        className={
+          navOpened ? 'max-h-screen' : 'max-h-0 py-0 md:max-h-screen md:py-2'
+        }
+        isCollapsed={isCollapsed}
+        links={sidelinks}
+      />
       <Button
         onClick={() => setIsCollapsed((prev) => !prev)}
         size='icon'
         variant='outline'
-        className='rounded-full -right-5 top-1/2 absolute'
+        className='absolute -right-5 top-1/2 hidden rounded-full md:inline-flex'
       >
         <IconChevronsLeft
           stroke={1.5}
-          className={`w-5 h-5 ${isCollapsed ? 'rotate-180' : ''}`}
+          className={`h-5 w-5 ${isCollapsed ? 'rotate-180' : ''}`}
         />
       </Button>
     </div>