dify
This commit is contained in:
85
dify/web/app/components/base/dropdown/index.stories.tsx
Normal file
85
dify/web/app/components/base/dropdown/index.stories.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import { fn } from 'storybook/test'
|
||||
import { useState } from 'react'
|
||||
import Dropdown, { type Item } from '.'
|
||||
|
||||
const PRIMARY_ITEMS: Item[] = [
|
||||
{ value: 'rename', text: 'Rename' },
|
||||
{ value: 'duplicate', text: 'Duplicate' },
|
||||
]
|
||||
|
||||
const SECONDARY_ITEMS: Item[] = [
|
||||
{ value: 'archive', text: <span className="text-text-destructive">Archive</span> },
|
||||
{ value: 'delete', text: <span className="text-text-destructive">Delete</span> },
|
||||
]
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Navigation/Dropdown',
|
||||
component: Dropdown,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Small contextual menu with optional destructive section. Uses portal positioning utilities for precise placement.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
items: PRIMARY_ITEMS,
|
||||
secondItems: SECONDARY_ITEMS,
|
||||
},
|
||||
} satisfies Meta<typeof Dropdown>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const DropdownDemo = (props: React.ComponentProps<typeof Dropdown>) => {
|
||||
const [lastAction, setLastAction] = useState<string>('None')
|
||||
|
||||
return (
|
||||
<div className="flex h-[200px] flex-col items-center justify-center gap-4">
|
||||
<Dropdown
|
||||
{...props}
|
||||
onSelect={(item) => {
|
||||
setLastAction(String(item.value))
|
||||
props.onSelect?.(item)
|
||||
}}
|
||||
/>
|
||||
<div className="rounded-lg border border-divider-subtle bg-components-panel-bg px-3 py-2 text-xs text-text-secondary">
|
||||
Last action: <span className="font-mono text-text-primary">{lastAction}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Playground: Story = {
|
||||
render: args => <DropdownDemo {...args} />,
|
||||
args: {
|
||||
items: PRIMARY_ITEMS,
|
||||
secondItems: SECONDARY_ITEMS,
|
||||
onSelect: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomTrigger: Story = {
|
||||
render: args => (
|
||||
<DropdownDemo
|
||||
{...args}
|
||||
renderTrigger={open => (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 rounded-md border border-divider-subtle px-3 py-1.5 text-sm text-text-secondary hover:bg-state-base-hover-alt"
|
||||
>
|
||||
Actions
|
||||
<span className={`transition-transform ${open ? 'rotate-180' : ''}`}>
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
args: {
|
||||
items: PRIMARY_ITEMS,
|
||||
onSelect: fn(),
|
||||
},
|
||||
}
|
||||
122
dify/web/app/components/base/dropdown/index.tsx
Normal file
122
dify/web/app/components/base/dropdown/index.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
RiMoreFill,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import type { ActionButtonProps } from '@/app/components/base/action-button'
|
||||
|
||||
export type Item = {
|
||||
value: string | number
|
||||
text: string | React.JSX.Element
|
||||
}
|
||||
type DropdownProps = {
|
||||
items: Item[]
|
||||
secondItems?: Item[]
|
||||
onSelect: (item: Item) => void
|
||||
renderTrigger?: (open: boolean) => React.ReactNode
|
||||
triggerProps?: ActionButtonProps
|
||||
popupClassName?: string
|
||||
itemClassName?: string
|
||||
secondItemClassName?: string
|
||||
}
|
||||
const Dropdown: FC<DropdownProps> = ({
|
||||
items,
|
||||
onSelect,
|
||||
secondItems,
|
||||
renderTrigger,
|
||||
triggerProps,
|
||||
popupClassName,
|
||||
itemClassName,
|
||||
secondItemClassName,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleSelect = (item: Item) => {
|
||||
setOpen(false)
|
||||
onSelect(item)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
{
|
||||
renderTrigger
|
||||
? renderTrigger(open)
|
||||
: (
|
||||
<ActionButton
|
||||
{...triggerProps}
|
||||
className={cn(
|
||||
open && 'bg-divider-regular',
|
||||
triggerProps?.className,
|
||||
)}
|
||||
>
|
||||
<RiMoreFill className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className={popupClassName}>
|
||||
<div className='rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg text-sm text-text-secondary shadow-lg'>
|
||||
{
|
||||
!!items.length && (
|
||||
<div className='p-1'>
|
||||
{
|
||||
items.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
'flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
itemClassName,
|
||||
)}
|
||||
onClick={() => handleSelect(item)}
|
||||
>
|
||||
{item.text}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(!!items.length && !!secondItems?.length) && (
|
||||
<div className='h-px bg-divider-regular' />
|
||||
)
|
||||
}
|
||||
{
|
||||
!!secondItems?.length && (
|
||||
<div className='p-1'>
|
||||
{
|
||||
secondItems.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
'flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
secondItemClassName,
|
||||
)}
|
||||
onClick={() => handleSelect(item)}
|
||||
>
|
||||
{item.text}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default Dropdown
|
||||
Reference in New Issue
Block a user