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 { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuPortal
|
||||||
|
} from './DropdownBase';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
|
||||||
export interface DropdownItem {
|
export interface DropdownItem {
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
|
@ -23,6 +34,65 @@ export interface ButtonDropdownProps extends DropdownMenuProps {
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
closeOnSelect?: boolean;
|
closeOnSelect?: boolean;
|
||||||
onSelect?: (item: DropdownItem) => void;
|
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}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -84,7 +84,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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',
|
inset && 'pl-8',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
@ -100,7 +100,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
@ -124,7 +124,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}>
|
{...props}>
|
||||||
|
|
|
@ -75,7 +75,7 @@
|
||||||
--color-track-background: hsl(0 0% 89.8%);
|
--color-track-background: hsl(0 0% 89.8%);
|
||||||
|
|
||||||
--color-icon-color: var(--color-gray-dark);
|
--color-icon-color: var(--color-gray-dark);
|
||||||
--color-ring: var(--color-foreground-hover);
|
--color-ring: var(--color-item-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
@import './tailwindAnimations.css';
|
@import './tailwindAnimations.css';
|
||||||
|
|
Loading…
Reference in New Issue