diff --git a/src/ui/sidebar.test.tsx b/src/ui/sidebar.test.tsx new file mode 100644 index 00000000..2c86e95f --- /dev/null +++ b/src/ui/sidebar.test.tsx @@ -0,0 +1,26 @@ +import { expect, test } from "bun:test"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; + +import { SidebarItem } from "./sidebar.js"; + +test("asChild renders the consumer's link with the icon, label and count inside it", () => { + const host = document.createElement("div"); + document.body.append(host); + const root = createRoot(host); + act(() => { + root.render( + } count={3}> + Inbox + , + ); + }); + const link = host.querySelector("a"); + expect(link?.getAttribute("href")).toBe("/inbox"); + expect(link?.getAttribute("aria-current")).toBe("page"); + expect(link?.getAttribute("data-slot")).toBe("sidebar-item"); + expect(link?.querySelector("svg")).not.toBeNull(); + expect(link?.textContent).toBe("Inbox3"); + act(() => root.unmount()); + host.remove(); +}); diff --git a/src/ui/sidebar.tsx b/src/ui/sidebar.tsx index a160c75b..ef6aef0e 100644 --- a/src/ui/sidebar.tsx +++ b/src/ui/sidebar.tsx @@ -1,5 +1,6 @@ import { Slot } from "@radix-ui/react-slot"; import { PanelLeftClose, PanelLeftOpen } from "lucide-react"; +import { cloneElement, isValidElement } from "react"; import type * as React from "react"; import { cn } from "../lib/utils.js"; @@ -114,6 +115,33 @@ export type SidebarItemProps = React.ComponentProps<"a"> & { asChild?: boolean; }; +function sidebarItemParts( + icon: React.ReactNode, + label: React.ReactNode, + count: number | undefined, +) { + return ( + <> + {icon === undefined ? null : ( + + {icon} + + )} + + {label} + + {count === undefined ? null : ( + + {count} + + )} + + ); +} + export function SidebarItem({ active = false, icon, @@ -124,6 +152,16 @@ export function SidebarItem({ ...props }: SidebarItemProps) { const Comp = asChild ? Slot : "a"; + // Slot accepts one child, so with `asChild` the icon, label and count go + // inside the consumer's element (a router link) rather than beside it. + const content = + asChild && isValidElement<{ children?: React.ReactNode }>(children) + ? cloneElement( + children, + undefined, + sidebarItemParts(icon, children.props.children, count), + ) + : sidebarItemParts(icon, children, count); return (
  • - {icon === undefined ? null : ( - - {icon} - - )} - - {children} - - {count === undefined ? null : ( - - {count} - - )} + {content}
  • );