mirror of https://github.com/buster-so/buster.git
dropdown v1
This commit is contained in:
parent
07f4e32d6d
commit
448dfe778f
|
@ -0,0 +1,211 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import { Button } from '../buttons/Button';
|
||||
const meta: Meta<typeof Dropdown> = {
|
||||
title: 'Base/Dropdown',
|
||||
component: Dropdown,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
tags: ['autodocs']
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Dropdown>;
|
||||
|
||||
// Basic example with simple items
|
||||
export const Basic: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Profile',
|
||||
onClick: () => console.log('Profile clicked')
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Settings',
|
||||
onClick: () => console.log('Settings clicked'),
|
||||
shortcut: '⌘S'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Logout',
|
||||
onClick: () => console.log('Logout clicked'),
|
||||
shortcut: '⌘L'
|
||||
}
|
||||
],
|
||||
children: <Button>Open Menu</Button>
|
||||
}
|
||||
};
|
||||
|
||||
// Example with icons and shortcuts
|
||||
export const WithIconsAndShortcuts: Story = {
|
||||
args: {
|
||||
menuLabel: 'Menu Options',
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Profile',
|
||||
icon: '👤',
|
||||
shortcut: '⌘P',
|
||||
onClick: () => console.log('Profile clicked')
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Settings',
|
||||
icon: '⚙️',
|
||||
shortcut: '⌘S',
|
||||
onClick: () => console.log('Settings clicked')
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Logout',
|
||||
icon: '🚪',
|
||||
shortcut: '⌘L',
|
||||
onClick: () => console.log('Logout clicked')
|
||||
}
|
||||
],
|
||||
children: <Button>Menu with Icons</Button>
|
||||
}
|
||||
};
|
||||
|
||||
// Example with nested items
|
||||
export const WithNestedItems: Story = {
|
||||
args: {
|
||||
menuLabel: 'Nested Menu',
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Main Options',
|
||||
items: [
|
||||
{
|
||||
id: '1-1',
|
||||
label: 'Option 1',
|
||||
onClick: () => console.log('Option 1 clicked')
|
||||
},
|
||||
{
|
||||
id: '1-2',
|
||||
label: 'Option 2',
|
||||
onClick: () => console.log('Option 2 clicked')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'More Options',
|
||||
items: [
|
||||
{
|
||||
id: '2-1',
|
||||
label: 'Sub Option 1',
|
||||
onClick: () => console.log('Sub Option 1 clicked')
|
||||
},
|
||||
{
|
||||
id: '2-2',
|
||||
label: 'Sub Option 2',
|
||||
onClick: () => console.log('Sub Option 2 clicked')
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
children: <Button>Nested Menu</Button>
|
||||
}
|
||||
};
|
||||
|
||||
// Example with disabled items
|
||||
export const WithDisabledItems: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Available Option',
|
||||
onClick: () => console.log('Available clicked')
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Disabled Option',
|
||||
disabled: true,
|
||||
onClick: () => console.log('Should not be called')
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Another Available',
|
||||
onClick: () => console.log('Another clicked')
|
||||
}
|
||||
],
|
||||
children: <Button>Menu with Disabled Items</Button>
|
||||
}
|
||||
};
|
||||
|
||||
// Example with custom widths
|
||||
export const CustomWidth: Story = {
|
||||
args: {
|
||||
menuLabel: 'Custom Width Menu',
|
||||
minWidth: 300,
|
||||
maxWidth: 400,
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'This is a very long menu item that might need wrapping',
|
||||
onClick: () => console.log('Long item clicked')
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Short item',
|
||||
onClick: () => console.log('Short item clicked')
|
||||
}
|
||||
],
|
||||
children: <Button>Wide Menu</Button>
|
||||
}
|
||||
};
|
||||
|
||||
// Example with loading state
|
||||
export const WithLoadingItems: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Normal Item',
|
||||
onClick: () => console.log('Normal clicked')
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Loading Item',
|
||||
loading: true,
|
||||
onClick: () => console.log('Loading clicked')
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Another Normal',
|
||||
onClick: () => console.log('Another clicked')
|
||||
}
|
||||
],
|
||||
children: <Button>Menu with Loading</Button>
|
||||
}
|
||||
};
|
||||
|
||||
// Example with selection
|
||||
export const WithSelection: Story = {
|
||||
args: {
|
||||
selectType: 'single',
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Option 1',
|
||||
selected: true,
|
||||
onClick: () => console.log('Option 1 clicked')
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Option 2',
|
||||
onClick: () => console.log('Option 2 clicked')
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Option 3',
|
||||
onClick: () => console.log('Option 3 clicked')
|
||||
}
|
||||
],
|
||||
children: <Button>Selection Menu</Button>
|
||||
}
|
||||
};
|
|
@ -1,5 +1,16 @@
|
|||
import { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
|
||||
import React from 'react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuPortal
|
||||
} from './DropdownBase';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
export interface DropdownItem {
|
||||
label: React.ReactNode;
|
||||
|
@ -23,6 +34,65 @@ export interface ButtonDropdownProps extends DropdownMenuProps {
|
|||
maxWidth?: number;
|
||||
closeOnSelect?: boolean;
|
||||
onSelect?: (item: DropdownItem) => void;
|
||||
onClose?: () => void;
|
||||
onOpen?: () => void;
|
||||
}
|
||||
|
||||
export const Dropdown: React.FC<ButtonDropdownProps> = ({
|
||||
items = [],
|
||||
selectType = 'default',
|
||||
menuLabel,
|
||||
minWidth = 200,
|
||||
maxWidth,
|
||||
closeOnSelect = true,
|
||||
onSelect,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const renderItems = (items: DropdownItem[]) => {
|
||||
return items.map((item) => {
|
||||
if (item.items) {
|
||||
return (
|
||||
<DropdownMenuSub key={item.id}>
|
||||
<DropdownMenuTrigger>
|
||||
{item.icon && <span className="mr-2">{item.icon}</span>}
|
||||
<span>{item.label}</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>{renderItems(item.items)}</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={item.id}
|
||||
disabled={item.disabled}
|
||||
onClick={() => {
|
||||
if (item.onClick) item.onClick();
|
||||
if (onSelect) onSelect(item);
|
||||
}}>
|
||||
{item.icon && <span className="mr-2">{item.icon}</span>}
|
||||
<span>{item.label}</span>
|
||||
{item.shortcut && (
|
||||
<span className="ml-auto text-xs tracking-widest opacity-60">{item.shortcut}</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu {...props}>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{
|
||||
minWidth: minWidth,
|
||||
maxWidth: maxWidth
|
||||
}}>
|
||||
{menuLabel && <DropdownMenuLabel>{menuLabel}</DropdownMenuLabel>}
|
||||
{menuLabel && <DropdownMenuSeparator />}
|
||||
{renderItems(items)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -66,7 +66,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||
'bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
@ -84,7 +84,7 @@ const DropdownMenuItem = React.forwardRef<
|
|||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
'focus:bg-item-hover focus:text-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
|
@ -100,7 +100,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
|
@ -124,7 +124,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
--color-track-background: hsl(0 0% 89.8%);
|
||||
|
||||
--color-icon-color: var(--color-gray-dark);
|
||||
--color-ring: var(--color-foreground-hover);
|
||||
--color-ring: var(--color-item-hover);
|
||||
}
|
||||
|
||||
@import './tailwindAnimations.css';
|
||||
|
|
Loading…
Reference in New Issue