update breadcrumb

This commit is contained in:
Nate Kelley 2025-02-26 11:20:36 -07:00
parent fb20eb2759
commit c3d6f9ea67
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
3 changed files with 269 additions and 0 deletions

View File

@ -0,0 +1,60 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Breadcrumb } from './Breadcrumb';
import { BusterAppRoutes } from '@/routes/busterRoutes/busterAppRoutes';
const meta: Meta<typeof Breadcrumb> = {
title: 'Base/Breadcrumb',
component: Breadcrumb,
tags: ['autodocs'],
parameters: {
layout: 'centered'
}
};
export default meta;
type Story = StoryObj<typeof Breadcrumb>;
export const Default: Story = {
args: {
items: [
{ label: 'Home', route: { route: BusterAppRoutes.APP_ROOT } },
{ label: 'Datasets', route: { route: BusterAppRoutes.APP_DATASETS } },
{ label: 'Current Dataset' }
]
}
};
export const WithDropdown: Story = {
args: {
items: [
{ label: 'Home', route: { route: BusterAppRoutes.APP_ROOT } },
{
label: null,
dropdown: [
{ label: 'Dataset A', route: { route: BusterAppRoutes.APP_DATASETS } },
{ label: 'Dataset B', route: { route: BusterAppRoutes.APP_DATASETS } },
{ label: 'Dataset C', route: { route: BusterAppRoutes.APP_DATASETS } }
]
},
{ label: 'Current Dataset' }
]
}
};
export const CustomActiveIndex: Story = {
args: {
items: [
{ label: 'Home', route: { route: BusterAppRoutes.APP_ROOT } },
{ label: 'Datasets', route: { route: BusterAppRoutes.APP_DATASETS } },
{ label: 'Settings', route: { route: BusterAppRoutes.SETTINGS_GENERAL } },
{ label: 'Profile' }
],
activeIndex: 2
}
};
export const SingleItem: Story = {
args: {
items: [{ label: 'Home' }]
}
};

View File

@ -0,0 +1,108 @@
import React, { useMemo } from 'react';
import {
Breadcrumb as BreadcrumbBase,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis
} from './BreadcrumbBase';
import { createBusterRoute } from '@/routes/busterRoutes';
import { Dropdown, DropdownItem } from '../dropdown/Dropdown';
import Link from 'next/link';
type CreateBusterRouteParams = Parameters<typeof createBusterRoute>[0];
interface BreadcrumbItem {
label: string | null; //if null, it will be an ellipsis
route?: CreateBusterRouteParams;
dropdown?: { label: string; route: CreateBusterRouteParams }[];
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
activeIndex?: number; //default will be the last item
}
export const Breadcrumb = React.memo(
React.forwardRef<HTMLElement, BreadcrumbProps>(({ items, activeIndex }, ref) => {
const chosenIndex = activeIndex ?? items.length - 1;
const lastItemIndex = items.length - 1;
console.log('chosenIndex', chosenIndex);
return (
<BreadcrumbBase>
<BreadcrumbList>
{items.map((item, index) => (
<BreadcrumbItemSelector
key={index}
item={item}
isActive={chosenIndex === index}
isLast={index === lastItemIndex}
/>
))}
</BreadcrumbList>
</BreadcrumbBase>
);
})
);
const BreadcrumbItemSelector: React.FC<{
item: BreadcrumbItem;
isActive: boolean;
isLast: boolean;
}> = ({ item, isActive, isLast }) => {
const ChosenComponent = useMemo(() => {
if (item.dropdown) {
return <BreadcrumbDropdown items={item.dropdown} />;
}
if (isActive) {
return <BreadcrumbPage>{item.label}</BreadcrumbPage>;
}
return (
<BreadcrumbLink asChild>
{item.route ? (
<Link href={createBusterRoute(item.route)}>{item.label}</Link>
) : (
<span>{item.label}</span>
)}
</BreadcrumbLink>
);
}, [isActive, item.label, item.route, item.dropdown]);
return (
<>
<BreadcrumbItem>{ChosenComponent}</BreadcrumbItem>
{!isLast && <BreadcrumbSeparator />}
</>
);
};
const BreadcrumbDropdown: React.FC<{
items: { label: string; route: CreateBusterRouteParams }[];
}> = ({ items }) => {
const dropdownItems: DropdownItem[] = useMemo(() => {
return items.map((item) => {
const route = createBusterRoute(item.route);
return {
label: <Link href={route}>{item.label}</Link>,
value: route
};
});
}, [items]);
return (
<Dropdown items={dropdownItems}>
<div className="flex cursor-pointer items-center">
<BreadcrumbEllipsis className="h-4 w-4" />
<span className="sr-only">Toggle menu</span>
</div>
</Dropdown>
);
};
Breadcrumb.displayName = 'Breadcrumb';

View File

@ -0,0 +1,101 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, Dots } from '../icons';
import Link from 'next/link';
import { cn } from '@/lib/utils';
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<'nav'> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = 'Breadcrumb';
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<'ol'>>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
'text-gray-dark flex flex-wrap items-center gap-1.5 text-base break-words sm:gap-2.5',
className
)}
{...props}
/>
)
);
BreadcrumbList.displayName = 'BreadcrumbList';
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<'li'>>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn('inline-flex items-center gap-1.5', className)} {...props} />
)
);
BreadcrumbItem.displayName = 'BreadcrumbItem';
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<'a'> & {
asChild?: boolean;
}
>(({ asChild, children, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'a';
return (
<Comp ref={ref} className={cn('hover:text-foreground transition-colors', className)} {...props}>
{children}
</Comp>
);
});
BreadcrumbLink.displayName = 'BreadcrumbLink';
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<'span'>>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn('text-foreground font-normal', className)}
{...props}
/>
)
);
BreadcrumbPage.displayName = 'BreadcrumbPage';
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<'li'>) => (
<li
role="presentation"
aria-hidden="true"
className={cn('text-icon-size [&>svg]:h-2.5 [&>svg]:w-2.5', className)}
{...props}>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
<span
role="presentation"
aria-hidden="true"
className={cn('hover:text-foreground flex h-9 w-9 items-center justify-center', className)}
{...props}>
<div className="text-icon-size flex">
<Dots />
</div>
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis
};