mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
201 lines
5.4 KiB
TypeScript
201 lines
5.4 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import * as React from 'react'
|
|
|
|
const DropdownMenuContext = React.createContext({
|
|
open: false,
|
|
onOpenChange: (_open: boolean) => {},
|
|
})
|
|
|
|
type DropdownMenuProps = {
|
|
children?: ReactNode
|
|
open?: boolean
|
|
onOpenChange?: (open: boolean) => void
|
|
}
|
|
|
|
type DropdownMenuTriggerProps = React.HTMLAttributes<HTMLElement> & {
|
|
children?: ReactNode
|
|
nativeButton?: boolean
|
|
render?: React.ReactElement
|
|
}
|
|
|
|
type DropdownMenuContentProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
children?: ReactNode
|
|
placement?: string
|
|
sideOffset?: number
|
|
alignOffset?: number
|
|
popupClassName?: string
|
|
}
|
|
|
|
export const DropdownMenu = ({ children, open, onOpenChange }: DropdownMenuProps) => {
|
|
const [localOpen, setLocalOpen] = React.useState(false)
|
|
const resolvedOpen = open ?? localOpen
|
|
const handleOpenChange = React.useCallback(
|
|
(nextOpen: boolean) => {
|
|
setLocalOpen(nextOpen)
|
|
onOpenChange?.(nextOpen)
|
|
},
|
|
[onOpenChange],
|
|
)
|
|
|
|
return (
|
|
<DropdownMenuContext.Provider value={{ open: resolvedOpen, onOpenChange: handleOpenChange }}>
|
|
<div data-testid="dropdown-menu" data-open={String(resolvedOpen)}>
|
|
{children}
|
|
</div>
|
|
</DropdownMenuContext.Provider>
|
|
)
|
|
}
|
|
|
|
export const DropdownMenuTrigger = ({
|
|
children,
|
|
render,
|
|
nativeButton: _nativeButton,
|
|
onClick,
|
|
...props
|
|
}: DropdownMenuTriggerProps) => {
|
|
const { open, onOpenChange } = React.useContext(DropdownMenuContext)
|
|
const node = render ?? children
|
|
const isNativeButton = React.isValidElement(node) && node.type === 'button'
|
|
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
onClick?.(event)
|
|
if (!event.defaultPrevented) onOpenChange(!open)
|
|
}
|
|
|
|
if (React.isValidElement(node)) {
|
|
const triggerElement = node as React.ReactElement<Record<string, unknown>>
|
|
const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes<HTMLElement> & {
|
|
'data-testid'?: string
|
|
}
|
|
const triggerProps = props as React.HTMLAttributes<HTMLElement> & { 'data-testid'?: string }
|
|
const role =
|
|
childProps.role ??
|
|
triggerProps.role ??
|
|
(!isNativeButton && (childProps['aria-label'] || triggerProps['aria-label'])
|
|
? 'button'
|
|
: undefined)
|
|
return React.cloneElement(
|
|
triggerElement,
|
|
{
|
|
...props,
|
|
...childProps,
|
|
'data-testid':
|
|
childProps['data-testid'] ?? triggerProps['data-testid'] ?? 'dropdown-menu-trigger',
|
|
role,
|
|
tabIndex:
|
|
childProps.tabIndex ?? triggerProps.tabIndex ?? (role === 'button' ? 0 : undefined),
|
|
onClick: (event: React.MouseEvent<HTMLElement>) => {
|
|
childProps.onClick?.(event)
|
|
handleClick(event)
|
|
},
|
|
},
|
|
render ? (children ?? childProps.children) : childProps.children,
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div
|
|
data-testid="dropdown-menu-trigger"
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={handleClick}
|
|
{...props}
|
|
>
|
|
{node}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const DropdownMenuContent = ({
|
|
children,
|
|
className,
|
|
popupClassName,
|
|
placement,
|
|
sideOffset,
|
|
alignOffset,
|
|
...props
|
|
}: DropdownMenuContentProps) => {
|
|
const { open } = React.useContext(DropdownMenuContext)
|
|
if (!open) return null
|
|
|
|
return (
|
|
<div
|
|
data-testid="dropdown-menu-content"
|
|
data-placement={placement}
|
|
data-side-offset={sideOffset}
|
|
data-align-offset={alignOffset}
|
|
className={className || popupClassName}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const DropdownMenuItem = ({
|
|
children,
|
|
onClick,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement> & { children?: ReactNode }) => (
|
|
<div role="menuitem" onClick={onClick} {...props}>
|
|
{children}
|
|
</div>
|
|
)
|
|
|
|
export const DropdownMenuRadioGroup = ({
|
|
children,
|
|
onValueChange,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement> & {
|
|
children?: ReactNode
|
|
value?: unknown
|
|
onValueChange?: (value: unknown) => void
|
|
}) => (
|
|
<div role="radiogroup" {...props} data-on-value-change={onValueChange ? 'true' : undefined}>
|
|
{React.Children.map(children, (child) => {
|
|
if (!React.isValidElement(child)) return child
|
|
return React.cloneElement(
|
|
child as React.ReactElement<{ __onValueChange?: (value: unknown) => void }>,
|
|
{ __onValueChange: onValueChange },
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
|
|
export const DropdownMenuRadioItem = ({
|
|
children,
|
|
value,
|
|
onClick,
|
|
__onValueChange,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement> & {
|
|
children?: ReactNode
|
|
value?: unknown
|
|
__onValueChange?: (value: unknown) => void
|
|
}) => (
|
|
<div
|
|
role="radio"
|
|
onClick={(event) => {
|
|
onClick?.(event)
|
|
__onValueChange?.(value)
|
|
}}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
|
|
export const DropdownMenuRadioItemIndicator = ({ children }: { children?: ReactNode }) => (
|
|
<>{children}</>
|
|
)
|
|
export const DropdownMenuCheckboxItem = DropdownMenuItem
|
|
export const DropdownMenuCheckboxItemIndicator = ({ children }: { children?: ReactNode }) => (
|
|
<>{children}</>
|
|
)
|
|
export const DropdownMenuLabel = ({ children }: { children?: ReactNode }) => <>{children}</>
|
|
export const DropdownMenuSeparator = (props: React.HTMLAttributes<HTMLDivElement>) => (
|
|
<div role="separator" {...props} />
|
|
)
|
|
export const DropdownMenuSub = ({ children }: { children?: ReactNode }) => <>{children}</>
|
|
export const DropdownMenuSubTrigger = DropdownMenuItem
|
|
export const DropdownMenuSubContent = ({ children }: { children?: ReactNode }) => <>{children}</>
|