shadcn-ui / ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com
MIT License
73.75k stars 4.54k forks source link

[help wanted]: Unable to control multiple sidebars within one Sidebar provider #5651

Open JolomiTee opened 5 days ago

JolomiTee commented 5 days ago

Describe the bug

I would like to have 2 or 3 sidebars within my project using the shadcn-ui sidebar component. Firstly, its possible to display them as they should look, for example:

1

What i discovered is that only one Sidebar provider can be used to display the sidebars, i have tried using multiple Sidebar providers for each sidebar but they all return a weird space on the page, but i wont show that right now. What I need help with is this: here is my code with nothing much going on, only displaying the sidebars in the positions I want them.

carbon

I want to be able to toggle the open states of each sidebar individually, but that is not working. for context, if i set the collapsible for all of them to "icon", they will all collapse into the icon state on the click of a single trigger found within the SidebarProvider

![image](https://github.com/user-att

Affected component/components

Sidebar, SidebarProvider

How to reproduce

Copy and paste this basic component:

import {
    Sidebar,
    SidebarHeader,
    SidebarInset,
    SidebarMenu,
    SidebarMenuButton,
    SidebarMenuItem,
    SidebarProvider,
    SidebarTrigger,
} from "../components/ui/sidebar";
import { Command } from "lucide-react";
import { Separator } from "../components/ui/separator";

const Sandbox = () => {
    return (
        <SidebarProvider>
            {/* sidebar 1 */}
            <Collapsible />

            {/* sidebar 2 */}
            <Channels />

            <SidebarInset>
                <header className="sticky top-0 flex shrink-0 items-center gap-2 border-b bg-background p-4">
                    {/* toggle the Collapsible */}
                    <button>Co</button>
                    <Separator orientation="vertical" className="mr-2 h-4" />
                    {/* toggle the Channels */}
                    <button>Ch</button>
                    <Separator orientation="vertical" className="mr-2 h-4" />
                    {/* toggle the Right */}
                    <button>R</button>
                    <SidebarTrigger className="-ml-1" />
                </header>

                <div className="flex">
                    <div className="flex flex-1 flex-col gap-4 p-4">
                        {Array.from({ length: 10 }).map((_, index) => (
                            <div
                                key={index}
                                className="aspect-video h-12 w-full bg-purple-400 rounded-lg bg-muted/50"
                            />
                        ))}
                    </div>

                    {/* sidebar 3 */}
                    <Right />
                </div>
            </SidebarInset>
        </SidebarProvider>
    );
};

export default Sandbox;

const Collapsible = () => {
    return (
        <Sidebar collapsible="icon" className="border-r bg-blue-600 z-50">
            <SidebarTrigger />
            <SidebarHeader>
                <SidebarMenu>
                    <SidebarMenuItem>
                        <SidebarMenuButton
                            size="lg"
                            asChild
                            className="md:h-8 md:p-0"
                        >
                            <a href="#">
                                <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
                                    <Command className="size-4" />
                                </div>
                                <div className="grid flex-1 text-left text-sm leading-tight">
                                    <span className="truncate font-semibold">
                                        Acme Inc
                                    </span>
                                    <span className="truncate text-xs">
                                        Collapsible Sidebar
                                    </span>
                                </div>
                            </a>
                        </SidebarMenuButton>
                    </SidebarMenuItem>
                </SidebarMenu>
            </SidebarHeader>
        </Sidebar>
    );
};

const Channels = () => {
    return (
        <Sidebar collapsible="icon" className="border-r bg-red-400">
            <SidebarHeader>
                <SidebarMenu>
                    <SidebarMenuItem>
                        <SidebarMenuButton
                            size="lg"
                            asChild
                            className="md:h-8 md:p-0"
                        >
                            <a href="#">
                                <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
                                    <Command className="size-4" />
                                </div>
                                <div className="grid flex-1 text-left text-sm leading-tight">
                                    <span className="truncate font-semibold">
                                        Acme Inc
                                    </span>
                                    <span className="truncate text-xs">
                                        Channels Sidebar
                                    </span>
                                </div>
                            </a>
                        </SidebarMenuButton>
                    </SidebarMenuItem>
                </SidebarMenu>
            </SidebarHeader>
        </Sidebar>
    );
};

const Right = () => {
    return (
        <Sidebar
            collapsible="icon"
            side="right"
            className="border-l bg-green-400"
        >
            <SidebarHeader>
                <SidebarMenu>
                    <SidebarMenuItem>
                        <SidebarMenuButton
                            size="lg"
                            asChild
                            className="md:h-8 md:p-0"
                        >
                            <a href="#">
                                <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
                                    <Command className="size-4" />
                                </div>
                                <div className="grid flex-1 text-left text-sm leading-tight">
                                    <span className="truncate font-semibold">
                                        Acme Inc
                                    </span>
                                    <span className="truncate text-xs">
                                        Right Sidebar
                                    </span>
                                </div>
                            </a>
                        </SidebarMenuButton>
                    </SidebarMenuItem>
                </SidebarMenu>
            </SidebarHeader>
        </Sidebar>
    );
};

Codesandbox/StackBlitz link

No response

Logs

No response

System Info

OS: Linux Mint 22
Browser: Microsoft Edge Browser
React Version: 19

Before submitting

Jomo178 commented 1 day ago

Hi @JolomiTee, did you find anything on this problem? I have the same problem as you. If I open one sidebar the other one gets opened too.

JolomiTee commented 1 day ago

@Jomo178 the only way I know of is to have a SidebarProvider wrapped around each individual sidebar instead of the whole App. Doing that, you can set individual widths, collapsible state too and most of all, control them individually. The only downside I have seen with this is that it takes away the mobile responsiveness of the sidebar... Whereby the Sheet component doesn't come out anymore. This might be due to my many tweakings of the sidebar.tsx file which I had to do to get what I wanted to achieve.

Jacksonmills commented 1 day ago

Another option is to track each sidebar via an id on the provider and then you can track/toggle specific matching sidebars/toggles

You would need to make the state a bit more complex but it's definitely doable

Jomo178 commented 1 day ago

@Jacksonmills you're right! The sidebar state is safed via cookies. So just change the SidebarProvider to take a custom name and you can controll them one by one. Change the setOpen so open via the custom name and not the setted cookie name. Hope this helps. @JolomiTee


const SidebarProvider = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div"> & {
    defaultOpen?: boolean;
    open?: boolean;
    onOpenChange?: (open: boolean) => void;
    name: string;
  }
>(
  (
    {
      defaultOpen = true,
      open: openProp,
      onOpenChange: setOpenProp,
      className,
      style,
      children,
      name,
      ...props
    },
    ref
  ) => {
    const isMobile = useIsMobile();
    const [openMobile, setOpenMobile] = React.useState(false);

    // This is the internal state of the sidebar.
    // We use openProp and setOpenProp for control from outside the component.
    const [_open, _setOpen] = React.useState(() => {
      const cookieValue = document.cookie
        .split("; ")
        .find((row) => row.startsWith(`${name}:state=`))
        ?.split("=")[1];
      return cookieValue === "true" ? true : defaultOpen;
    });
    const open = openProp ?? _open;
    const setOpen = React.useCallback(
      (value: boolean | ((value: boolean) => boolean)) => {
        const openState = typeof value === "function" ? value(open) : value;
        if (setOpenProp) {
          setOpenProp(openState);
        } else {
          _setOpen(openState);
        }

        // This sets the cookie to keep the sidebar state.
        document.cookie = `${name}:state=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
      },
      [setOpenProp, open, name]
    );

//Rest of the sidebar code..

//Example
 <SidebarProvider name="YOURE SIDEBAR NAME">
        <Sidebar variant="floating" collapsible="icon" side="right">
          <SidebarContent>
          </SidebarContent>
       </Sidebar>
</SidebarProvider>
JolomiTee commented 1 day ago

Thanks, @Jomo178 and @Jacksonmills, I will try it out and get back as soon as i can

JolomiTee commented 1 day ago

@Jomo178 @Jacksonmills I was able to adjust mine with the solution @Jomo178 implemented.

So from the screenshot below, I have wrapped each sidebar with the Provider, passed unique names to each, and then put the SidebarTrigger within the Sidebar and with that, each one is isolated and controlled individually

Screenshot from 2024-11-03 22-11-01

image

What I want to test out now is how it works on mobile... because right now, it all disappears.. likely into the sheet form, waiting to be triggered and there is no way to trigger it yet with my current UI :)