admin/components/heroui/Dropdown.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

156 lines
5.0 KiB
TypeScript

'use client'
import type { ComponentProps, ReactElement, ReactNode } from 'react'
import { Children, Fragment, isValidElement } from 'react'
import { Description, Dropdown as HeroDropdown, Header, Label, Separator } from '@heroui/react'
type DropdownKey = string | number
export interface DropdownItemProps {
children?: ReactNode
className?: string
color?: 'default' | 'danger'
description?: ReactNode
key?: DropdownKey
startContent?: ReactNode
textValue?: string
onPress?: () => void
}
interface DropdownMenuProps {
children?: ReactNode
'aria-label'?: string
closeOnSelect?: boolean
disabledKeys?: Iterable<DropdownKey>
disallowEmptySelection?: boolean
selectedKeys?: 'all' | Iterable<DropdownKey>
selectionMode?: 'none' | 'single' | 'multiple'
variant?: string
onAction?: (key: DropdownKey) => void
onSelectionChange?: (keys: 'all' | Set<DropdownKey>) => void
}
interface DropdownSectionProps {
children?: ReactNode
showDivider?: boolean
title?: ReactNode
}
interface DropdownProps {
children?: ReactNode
placement?: ComponentProps<typeof HeroDropdown.Popover>['placement'] | 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'
trigger?: 'press' | 'longPress'
}
export function DropdownTrigger({ children }: { children?: ReactNode }) {
return children
}
export function DropdownMenu({ children }: DropdownMenuProps) {
return children
}
export function DropdownItem({ children }: DropdownItemProps) {
return children
}
export function DropdownSection({ children }: DropdownSectionProps) {
return children
}
function itemTextValue(item: ReactElement<DropdownItemProps>, id: string): string {
if (item.props.textValue) return item.props.textValue
if (typeof item.props.children === 'string' || typeof item.props.children === 'number') return String(item.props.children)
return id
}
function renderItem(item: ReactElement<DropdownItemProps>, index: number, showIndicator: boolean) {
const id = normalizeElementKey(item.key, index)
const label = <Label>{item.props.children}</Label>
const description = item.props.description ? <Description>{item.props.description}</Description> : null
return (
<HeroDropdown.Item
key={id}
className={item.props.className}
id={id}
textValue={itemTextValue(item, id)}
variant={item.props.color === 'danger' ? 'danger' : undefined}
onAction={item.props.onPress}
>
{showIndicator ? <HeroDropdown.ItemIndicator /> : null}
{item.props.startContent}
{description ? (
<div className="flex min-w-0 flex-col">
{label}
{description}
</div>
) : (
label
)}
</HeroDropdown.Item>
)
}
function normalizeElementKey(key: ReactElement['key'], index: number): string {
if (key == null) return String(index)
const value = String(key)
return value.startsWith('.$') ? value.slice(2) : value
}
function renderMenuChildren(children: ReactNode, showIndicator: boolean): ReactNode {
return Children.toArray(children).map((child, index) => {
if (!isValidElement(child)) return null
if (child.type === DropdownSection) {
const section = child as ReactElement<DropdownSectionProps>
return (
<Fragment key={normalizeElementKey(section.key, index)}>
<HeroDropdown.Section>
{section.props.title ? <Header>{section.props.title}</Header> : null}
{renderMenuChildren(section.props.children, showIndicator)}
</HeroDropdown.Section>
{section.props.showDivider ? <Separator /> : null}
</Fragment>
)
}
return renderItem(child as ReactElement<DropdownItemProps>, index, showIndicator)
})
}
export function Dropdown({ children, placement, trigger }: DropdownProps) {
const parts = Children.toArray(children).filter(isValidElement)
const triggerElement = parts.find((part) => part.type === DropdownTrigger) as ReactElement<{ children?: ReactNode }> | undefined
const menuElement = parts.find((part) => part.type === DropdownMenu) as ReactElement<DropdownMenuProps> | undefined
if (!triggerElement || !menuElement) return null
const menuChildren = menuElement.props.children
const menuProps = {
'aria-label': menuElement.props['aria-label'],
disabledKeys: menuElement.props.disabledKeys,
disallowEmptySelection: menuElement.props.disallowEmptySelection,
selectedKeys: menuElement.props.selectedKeys,
selectionMode: menuElement.props.selectionMode,
onAction: menuElement.props.onAction,
onSelectionChange: menuElement.props.onSelectionChange,
}
const showIndicator = menuProps.selectionMode === 'single' || menuProps.selectionMode === 'multiple'
const normalizedPlacement = placement?.replace('-', ' ') as ComponentProps<typeof HeroDropdown.Popover>['placement']
return (
<HeroDropdown trigger={trigger}>
{triggerElement.props.children}
<HeroDropdown.Popover placement={normalizedPlacement}>
<HeroDropdown.Menu {...menuProps}>{renderMenuChildren(menuChildren, showIndicator)}</HeroDropdown.Menu>
</HeroDropdown.Popover>
</HeroDropdown>
)
}