refactor(TextEditor, Dropdown, UserDropdown): improve dropdown trigger implementation and styling
- Updated `TextEditor` component to use consistent class names for dropdown triggers, enhancing maintainability. - Refactored `Dropdown` component to resolve trigger properties more reliably, ensuring proper rendering of button-like triggers. - Modified `UserDropdown` to utilize `buttonVariants` for styling the dropdown trigger, improving visual consistency across components.
This commit is contained in:
parent
8967a48f94
commit
5404974c32
@ -20,6 +20,12 @@ import FileUpload from '@/components/media/FileUpload'
|
||||
import ConsumerModal from '@/components/consumer/ConsumerModal'
|
||||
import { fileAddress, coerceToString } from '@/helpers'
|
||||
import { cn } from '@/lib/cn'
|
||||
import { buttonVariants } from '@heroui/react'
|
||||
|
||||
const CONSUMER_TOOLBAR_TRIGGER_CLASS =
|
||||
'min-h-10 rounded-[10px] border border-consumer-border bg-consumer-surface px-3 text-sm font-medium text-consumer-text outline-none'
|
||||
|
||||
const ADMIN_TOOLBAR_TRIGGER_CLASS = cn(buttonVariants({ size: 'sm', variant: 'secondary' }), 'outline-none')
|
||||
|
||||
interface TextEditorProps {
|
||||
label?: string
|
||||
@ -186,14 +192,11 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
||||
{isConsumer ? (
|
||||
<>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<button
|
||||
<DropdownTrigger
|
||||
aria-label="عنوان"
|
||||
className="min-h-10 rounded-[10px] border border-consumer-border bg-consumer-surface px-3 text-sm font-medium text-consumer-text"
|
||||
type="button"
|
||||
className={CONSUMER_TOOLBAR_TRIGGER_CLASS}
|
||||
>
|
||||
عنوان
|
||||
</button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="نوع عنوان">
|
||||
<DropdownItem
|
||||
@ -222,14 +225,11 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<button
|
||||
<DropdownTrigger
|
||||
aria-label="رنگ متن"
|
||||
className="min-h-10 rounded-[10px] border border-consumer-border bg-consumer-surface px-3 text-sm font-medium text-consumer-text"
|
||||
type="button"
|
||||
className={CONSUMER_TOOLBAR_TRIGGER_CLASS}
|
||||
>
|
||||
رنگ
|
||||
</button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="رنگ متن">
|
||||
{CONSUMER_TEXT_COLORS.map((color) => (
|
||||
@ -250,14 +250,11 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<button
|
||||
<DropdownTrigger
|
||||
aria-label="فهرست"
|
||||
className="min-h-10 rounded-[10px] border border-consumer-border bg-consumer-surface px-3 text-sm font-medium text-consumer-text"
|
||||
type="button"
|
||||
className={CONSUMER_TOOLBAR_TRIGGER_CLASS}
|
||||
>
|
||||
فهرست
|
||||
</button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="نوع فهرست">
|
||||
<DropdownItem
|
||||
@ -278,14 +275,7 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
||||
) : (
|
||||
<>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
font
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownTrigger className={ADMIN_TOOLBAR_TRIGGER_CLASS}>font</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Static Actions">
|
||||
<DropdownItem
|
||||
key="bold"
|
||||
@ -314,14 +304,7 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
heading
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownTrigger className={ADMIN_TOOLBAR_TRIGGER_CLASS}>heading</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Static Actions">
|
||||
<DropdownItem
|
||||
key="H1"
|
||||
@ -374,14 +357,7 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
align
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownTrigger className={ADMIN_TOOLBAR_TRIGGER_CLASS}>align</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Static Actions">
|
||||
<DropdownItem
|
||||
key="left"
|
||||
@ -410,14 +386,7 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
link
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownTrigger className={ADMIN_TOOLBAR_TRIGGER_CLASS}>link</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Static Actions">
|
||||
<DropdownItem
|
||||
key="link"
|
||||
@ -434,14 +403,7 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
list
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownTrigger className={ADMIN_TOOLBAR_TRIGGER_CLASS}>list</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Static Actions">
|
||||
<DropdownItem
|
||||
key="ordered"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { ComponentProps, ReactElement, ReactNode } from 'react'
|
||||
import type { ComponentProps, CSSProperties, ReactElement, ReactNode } from 'react'
|
||||
|
||||
import { Children, Fragment, isValidElement } from 'react'
|
||||
import { Description, Dropdown as HeroDropdown, Header, Label, Separator } from '@heroui/react'
|
||||
@ -37,13 +37,21 @@ interface DropdownSectionProps {
|
||||
title?: ReactNode
|
||||
}
|
||||
|
||||
interface DropdownTriggerProps {
|
||||
children?: ReactNode
|
||||
'aria-label'?: string
|
||||
className?: string
|
||||
isDisabled?: boolean
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
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 }) {
|
||||
export function DropdownTrigger({ children }: DropdownTriggerProps) {
|
||||
return children
|
||||
}
|
||||
|
||||
@ -124,13 +132,65 @@ function renderMenuChildren(children: ReactNode, showIndicator: boolean): ReactN
|
||||
})
|
||||
}
|
||||
|
||||
function isButtonLikeTrigger(child: ReactElement): boolean {
|
||||
if (typeof child.type === 'string') return child.type === 'button'
|
||||
|
||||
const displayName =
|
||||
typeof child.type === 'function' || typeof child.type === 'object'
|
||||
? (child.type as { displayName?: string; name?: string }).displayName ||
|
||||
(child.type as { name?: string }).name
|
||||
: undefined
|
||||
|
||||
return displayName === 'ConsumerButton' || displayName === 'Button'
|
||||
}
|
||||
|
||||
/** Button/`button` داخل MenuTrigger reliably باز نمیشود؛ محتوا را به Dropdown.Trigger واقعی منتقل میکنیم. */
|
||||
function resolveTrigger(triggerElement: ReactElement<DropdownTriggerProps>): {
|
||||
'aria-label'?: string
|
||||
className?: string
|
||||
content: ReactNode
|
||||
isDisabled?: boolean
|
||||
style?: CSSProperties
|
||||
} {
|
||||
const { children, className, isDisabled, style } = triggerElement.props
|
||||
const ariaLabel = triggerElement.props['aria-label']
|
||||
const onlyChild = Children.count(children) === 1 ? Children.toArray(children)[0] : null
|
||||
|
||||
if (isValidElement(onlyChild) && isButtonLikeTrigger(onlyChild)) {
|
||||
const nested = onlyChild.props as {
|
||||
'aria-label'?: string
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
return {
|
||||
'aria-label': nested['aria-label'] ?? ariaLabel,
|
||||
className: nested.className ?? className,
|
||||
content: nested.children,
|
||||
isDisabled: nested.disabled ?? isDisabled,
|
||||
style: nested.style ?? style,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'aria-label': ariaLabel,
|
||||
className,
|
||||
content: children,
|
||||
isDisabled,
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
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 triggerElement = parts.find((part) => part.type === DropdownTrigger) as ReactElement<DropdownTriggerProps> | undefined
|
||||
const menuElement = parts.find((part) => part.type === DropdownMenu) as ReactElement<DropdownMenuProps> | undefined
|
||||
|
||||
if (!triggerElement || !menuElement) return null
|
||||
|
||||
const resolvedTrigger = resolveTrigger(triggerElement)
|
||||
const menuChildren = menuElement.props.children
|
||||
const menuProps = {
|
||||
'aria-label': menuElement.props['aria-label'],
|
||||
@ -146,7 +206,14 @@ export function Dropdown({ children, placement, trigger }: DropdownProps) {
|
||||
|
||||
return (
|
||||
<HeroDropdown trigger={trigger}>
|
||||
{triggerElement.props.children}
|
||||
<HeroDropdown.Trigger
|
||||
aria-label={resolvedTrigger['aria-label']}
|
||||
className={resolvedTrigger.className}
|
||||
isDisabled={resolvedTrigger.isDisabled}
|
||||
style={resolvedTrigger.style}
|
||||
>
|
||||
{resolvedTrigger.content}
|
||||
</HeroDropdown.Trigger>
|
||||
<HeroDropdown.Popover placement={normalizedPlacement}>
|
||||
<HeroDropdown.Menu {...menuProps}>{renderMenuChildren(menuChildren, showIndicator)}</HeroDropdown.Menu>
|
||||
</HeroDropdown.Popover>
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
'use client'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useMemo } from 'react'
|
||||
import { buttonVariants } from '@heroui/react'
|
||||
|
||||
import { Dropdown, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger } from '@/components/heroui/Dropdown'
|
||||
import { Avatar } from '@/components/heroui/Avatar'
|
||||
import Button from '@/components/formElements/Button'
|
||||
import LogoutIcon from '@/components/icons/LogoutIcon'
|
||||
import { fileAddress } from '@/helpers'
|
||||
import { resolveUserAvatarSrc } from '@/lib/resolveUserAvatarSrc'
|
||||
import useAuth from '@/hooks/useAuth'
|
||||
import useAlertModal from '@/hooks/useAlertModal'
|
||||
import EditIcon from '@/components/icons/EditIcon'
|
||||
import { cn } from '@/lib/cn'
|
||||
|
||||
const UserDropdown = () => {
|
||||
const { user, logout } = useAuth()
|
||||
@ -25,13 +26,16 @@ const UserDropdown = () => {
|
||||
return (
|
||||
<>
|
||||
<Dropdown placement="bottom-start">
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
iconOnly
|
||||
<DropdownTrigger
|
||||
aria-label="منوی حساب کاربری"
|
||||
className="rounded-full p-0"
|
||||
radius="full"
|
||||
variant="light"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
isIconOnly: true,
|
||||
size: 'lg',
|
||||
variant: 'tertiary',
|
||||
}),
|
||||
'rounded-full p-0 outline-none'
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
isBordered
|
||||
@ -39,7 +43,6 @@ const UserDropdown = () => {
|
||||
size="lg"
|
||||
src={avatarSrc}
|
||||
/>
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownMenu
|
||||
|
||||
Loading…
Reference in New Issue
Block a user