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 ConsumerModal from '@/components/consumer/ConsumerModal'
|
||||||
import { fileAddress, coerceToString } from '@/helpers'
|
import { fileAddress, coerceToString } from '@/helpers'
|
||||||
import { cn } from '@/lib/cn'
|
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 {
|
interface TextEditorProps {
|
||||||
label?: string
|
label?: string
|
||||||
@ -186,14 +192,11 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
|||||||
{isConsumer ? (
|
{isConsumer ? (
|
||||||
<>
|
<>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger
|
||||||
<button
|
|
||||||
aria-label="عنوان"
|
aria-label="عنوان"
|
||||||
className="min-h-10 rounded-[10px] border border-consumer-border bg-consumer-surface px-3 text-sm font-medium text-consumer-text"
|
className={CONSUMER_TOOLBAR_TRIGGER_CLASS}
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
عنوان
|
عنوان
|
||||||
</button>
|
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu aria-label="نوع عنوان">
|
<DropdownMenu aria-label="نوع عنوان">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
@ -222,14 +225,11 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger
|
||||||
<button
|
|
||||||
aria-label="رنگ متن"
|
aria-label="رنگ متن"
|
||||||
className="min-h-10 rounded-[10px] border border-consumer-border bg-consumer-surface px-3 text-sm font-medium text-consumer-text"
|
className={CONSUMER_TOOLBAR_TRIGGER_CLASS}
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
رنگ
|
رنگ
|
||||||
</button>
|
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu aria-label="رنگ متن">
|
<DropdownMenu aria-label="رنگ متن">
|
||||||
{CONSUMER_TEXT_COLORS.map((color) => (
|
{CONSUMER_TEXT_COLORS.map((color) => (
|
||||||
@ -250,14 +250,11 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger
|
||||||
<button
|
|
||||||
aria-label="فهرست"
|
aria-label="فهرست"
|
||||||
className="min-h-10 rounded-[10px] border border-consumer-border bg-consumer-surface px-3 text-sm font-medium text-consumer-text"
|
className={CONSUMER_TOOLBAR_TRIGGER_CLASS}
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
فهرست
|
فهرست
|
||||||
</button>
|
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu aria-label="نوع فهرست">
|
<DropdownMenu aria-label="نوع فهرست">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
@ -278,14 +275,7 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger className={ADMIN_TOOLBAR_TRIGGER_CLASS}>font</DropdownTrigger>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
font
|
|
||||||
</Button>
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu aria-label="Static Actions">
|
<DropdownMenu aria-label="Static Actions">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="bold"
|
key="bold"
|
||||||
@ -314,14 +304,7 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger className={ADMIN_TOOLBAR_TRIGGER_CLASS}>heading</DropdownTrigger>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
heading
|
|
||||||
</Button>
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu aria-label="Static Actions">
|
<DropdownMenu aria-label="Static Actions">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="H1"
|
key="H1"
|
||||||
@ -374,14 +357,7 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger className={ADMIN_TOOLBAR_TRIGGER_CLASS}>align</DropdownTrigger>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
align
|
|
||||||
</Button>
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu aria-label="Static Actions">
|
<DropdownMenu aria-label="Static Actions">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="left"
|
key="left"
|
||||||
@ -410,14 +386,7 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger className={ADMIN_TOOLBAR_TRIGGER_CLASS}>link</DropdownTrigger>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
link
|
|
||||||
</Button>
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu aria-label="Static Actions">
|
<DropdownMenu aria-label="Static Actions">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="link"
|
key="link"
|
||||||
@ -434,14 +403,7 @@ const TextEditor = ({ label, required, value, onChange, className, variant = 'de
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger className={ADMIN_TOOLBAR_TRIGGER_CLASS}>list</DropdownTrigger>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
list
|
|
||||||
</Button>
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu aria-label="Static Actions">
|
<DropdownMenu aria-label="Static Actions">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="ordered"
|
key="ordered"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { ComponentProps, ReactElement, ReactNode } from 'react'
|
import type { ComponentProps, CSSProperties, ReactElement, ReactNode } from 'react'
|
||||||
|
|
||||||
import { Children, Fragment, isValidElement } from 'react'
|
import { Children, Fragment, isValidElement } from 'react'
|
||||||
import { Description, Dropdown as HeroDropdown, Header, Label, Separator } from '@heroui/react'
|
import { Description, Dropdown as HeroDropdown, Header, Label, Separator } from '@heroui/react'
|
||||||
@ -37,13 +37,21 @@ interface DropdownSectionProps {
|
|||||||
title?: ReactNode
|
title?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DropdownTriggerProps {
|
||||||
|
children?: ReactNode
|
||||||
|
'aria-label'?: string
|
||||||
|
className?: string
|
||||||
|
isDisabled?: boolean
|
||||||
|
style?: CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
interface DropdownProps {
|
interface DropdownProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
placement?: ComponentProps<typeof HeroDropdown.Popover>['placement'] | 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'
|
placement?: ComponentProps<typeof HeroDropdown.Popover>['placement'] | 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'
|
||||||
trigger?: 'press' | 'longPress'
|
trigger?: 'press' | 'longPress'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownTrigger({ children }: { children?: ReactNode }) {
|
export function DropdownTrigger({ children }: DropdownTriggerProps) {
|
||||||
return children
|
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) {
|
export function Dropdown({ children, placement, trigger }: DropdownProps) {
|
||||||
const parts = Children.toArray(children).filter(isValidElement)
|
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
|
const menuElement = parts.find((part) => part.type === DropdownMenu) as ReactElement<DropdownMenuProps> | undefined
|
||||||
|
|
||||||
if (!triggerElement || !menuElement) return null
|
if (!triggerElement || !menuElement) return null
|
||||||
|
|
||||||
|
const resolvedTrigger = resolveTrigger(triggerElement)
|
||||||
const menuChildren = menuElement.props.children
|
const menuChildren = menuElement.props.children
|
||||||
const menuProps = {
|
const menuProps = {
|
||||||
'aria-label': menuElement.props['aria-label'],
|
'aria-label': menuElement.props['aria-label'],
|
||||||
@ -146,7 +206,14 @@ export function Dropdown({ children, placement, trigger }: DropdownProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HeroDropdown trigger={trigger}>
|
<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.Popover placement={normalizedPlacement}>
|
||||||
<HeroDropdown.Menu {...menuProps}>{renderMenuChildren(menuChildren, showIndicator)}</HeroDropdown.Menu>
|
<HeroDropdown.Menu {...menuProps}>{renderMenuChildren(menuChildren, showIndicator)}</HeroDropdown.Menu>
|
||||||
</HeroDropdown.Popover>
|
</HeroDropdown.Popover>
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import { buttonVariants } from '@heroui/react'
|
||||||
|
|
||||||
import { Dropdown, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger } from '@/components/heroui/Dropdown'
|
import { Dropdown, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger } from '@/components/heroui/Dropdown'
|
||||||
import { Avatar } from '@/components/heroui/Avatar'
|
import { Avatar } from '@/components/heroui/Avatar'
|
||||||
import Button from '@/components/formElements/Button'
|
|
||||||
import LogoutIcon from '@/components/icons/LogoutIcon'
|
import LogoutIcon from '@/components/icons/LogoutIcon'
|
||||||
import { fileAddress } from '@/helpers'
|
import { fileAddress } from '@/helpers'
|
||||||
import { resolveUserAvatarSrc } from '@/lib/resolveUserAvatarSrc'
|
import { resolveUserAvatarSrc } from '@/lib/resolveUserAvatarSrc'
|
||||||
import useAuth from '@/hooks/useAuth'
|
import useAuth from '@/hooks/useAuth'
|
||||||
import useAlertModal from '@/hooks/useAlertModal'
|
import useAlertModal from '@/hooks/useAlertModal'
|
||||||
import EditIcon from '@/components/icons/EditIcon'
|
import EditIcon from '@/components/icons/EditIcon'
|
||||||
|
import { cn } from '@/lib/cn'
|
||||||
|
|
||||||
const UserDropdown = () => {
|
const UserDropdown = () => {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
@ -25,13 +26,16 @@ const UserDropdown = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dropdown placement="bottom-start">
|
<Dropdown placement="bottom-start">
|
||||||
<DropdownTrigger>
|
<DropdownTrigger
|
||||||
<Button
|
|
||||||
iconOnly
|
|
||||||
aria-label="منوی حساب کاربری"
|
aria-label="منوی حساب کاربری"
|
||||||
className="rounded-full p-0"
|
className={cn(
|
||||||
radius="full"
|
buttonVariants({
|
||||||
variant="light"
|
isIconOnly: true,
|
||||||
|
size: 'lg',
|
||||||
|
variant: 'tertiary',
|
||||||
|
}),
|
||||||
|
'rounded-full p-0 outline-none'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
isBordered
|
isBordered
|
||||||
@ -39,7 +43,6 @@ const UserDropdown = () => {
|
|||||||
size="lg"
|
size="lg"
|
||||||
src={avatarSrc}
|
src={avatarSrc}
|
||||||
/>
|
/>
|
||||||
</Button>
|
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
|
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user