refactor(ui): compose tooltip primitives and dedupe menu popup

This commit is contained in:
yyh
2026-03-02 18:03:25 +08:00
parent f83f84afac
commit c4fe93a8b8
3 changed files with 106 additions and 83 deletions

View File

@ -7,6 +7,7 @@ import { parsePlacement } from '@/app/components/base/ui/placement'
import { cn } from '@/utils/classnames'
export const DropdownMenu = Menu.Root
export const DropdownMenuPortal = Menu.Portal
export const DropdownMenuTrigger = Menu.Trigger
export const DropdownMenuSub = Menu.SubmenuRoot
export const DropdownMenuGroup = Menu.Group
@ -26,14 +27,22 @@ type DropdownMenuContentProps = {
popupClassName?: string
}
export function DropdownMenuContent({
type DropdownMenuPopupProps = Required<Pick<DropdownMenuContentProps, 'children'>> & {
placement: Placement
sideOffset: number
alignOffset: number
className?: string
popupClassName?: string
}
function DropdownMenuPopup({
children,
placement = 'bottom-end',
sideOffset = 4,
alignOffset = 0,
placement,
sideOffset,
alignOffset,
className,
popupClassName,
}: DropdownMenuContentProps) {
}: DropdownMenuPopupProps) {
const { side, align } = parsePlacement(placement)
return (
@ -43,7 +52,7 @@ export function DropdownMenuContent({
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn('outline-none', className)}
className={cn('isolate outline-none', className)}
>
<Menu.Popup
className={cn(
@ -59,6 +68,27 @@ export function DropdownMenuContent({
)
}
export function DropdownMenuContent({
children,
placement = 'bottom-end',
sideOffset = 4,
alignOffset = 0,
className,
popupClassName,
}: DropdownMenuContentProps) {
return (
<DropdownMenuPopup
placement={placement}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={className}
popupClassName={popupClassName}
>
{children}
</DropdownMenuPopup>
)
}
type DropdownMenuSubTriggerProps = React.ComponentPropsWithoutRef<typeof Menu.SubmenuTrigger> & {
destructive?: boolean
}
@ -98,28 +128,16 @@ export function DropdownMenuSubContent({
className,
popupClassName,
}: DropdownMenuSubContentProps) {
const { side, align } = parsePlacement(placement)
return (
<Menu.Portal>
<Menu.Positioner
side={side}
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn('outline-none', className)}
>
<Menu.Popup
className={cn(
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg py-1 text-sm text-text-secondary shadow-lg',
'origin-[var(--transform-origin)] transition-[transform,scale,opacity] data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0',
popupClassName,
)}
>
{children}
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
<DropdownMenuPopup
placement={placement}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={className}
popupClassName={popupClassName}
>
{children}
</DropdownMenuPopup>
)
}

View File

@ -6,61 +6,53 @@ import * as React from 'react'
import { parsePlacement } from '@/app/components/base/ui/placement'
import { cn } from '@/utils/classnames'
export type TooltipProps = {
position?: Placement
disabled?: boolean
popupContent?: React.ReactNode
children?: React.ReactNode
type TooltipContentVariant = 'default' | 'plain'
export type TooltipContentProps = {
children: React.ReactNode
placement?: Placement
sideOffset?: number
alignOffset?: number
className?: string
popupClassName?: string
noDecoration?: boolean
offset?: number
delay?: number
closeDelay?: number
}
variant?: TooltipContentVariant
} & Omit<React.ComponentPropsWithoutRef<typeof BaseTooltip.Popup>, 'children'>
const Tooltip = React.memo(({
position = 'top',
disabled = false,
popupContent,
export function TooltipContent({
children,
placement = 'top',
sideOffset = 8,
alignOffset = 0,
className,
popupClassName,
noDecoration,
offset = 8,
delay,
closeDelay,
}: TooltipProps) => {
const { side, align } = parsePlacement(position)
if (!popupContent || disabled)
return <>{children}</>
variant = 'default',
...props
}: TooltipContentProps) {
const { side, align } = parsePlacement(placement)
return (
<BaseTooltip.Root>
{React.isValidElement(children)
? <BaseTooltip.Trigger delay={delay} closeDelay={closeDelay} render={children} />
: <BaseTooltip.Trigger delay={delay} closeDelay={closeDelay} render={<span className="inline-flex" />}>{children}</BaseTooltip.Trigger>}
<BaseTooltip.Portal>
<BaseTooltip.Positioner
side={side}
align={align}
sideOffset={offset}
className="outline-none"
<BaseTooltip.Portal>
<BaseTooltip.Positioner
side={side}
align={align}
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn('isolate outline-none', className)}
>
<BaseTooltip.Popup
className={cn(
variant === 'default' && 'max-w-[300px] break-words rounded-md bg-components-panel-bg px-3 py-2 text-left text-text-tertiary shadow-lg system-xs-regular',
popupClassName,
)}
{...props}
>
<BaseTooltip.Popup
className={cn(
!noDecoration && 'max-w-[300px] break-words rounded-md bg-components-panel-bg px-3 py-2 text-left text-text-tertiary shadow-lg system-xs-regular',
popupClassName,
)}
>
{popupContent}
</BaseTooltip.Popup>
</BaseTooltip.Positioner>
</BaseTooltip.Portal>
</BaseTooltip.Root>
{children}
</BaseTooltip.Popup>
</BaseTooltip.Positioner>
</BaseTooltip.Portal>
)
})
Tooltip.displayName = 'Tooltip'
}
export const TooltipProvider = BaseTooltip.Provider
export default Tooltip
export const Tooltip = BaseTooltip.Root
export const TooltipTrigger = BaseTooltip.Trigger

View File

@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu'
import Tooltip from '@/app/components/base/ui/tooltip'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
import { Plan } from '@/app/components/billing/type'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContext } from '@/context/modal-context'
@ -83,14 +83,27 @@ function ComplianceDocActionVisual({
)
}
const canShowUpgradeTooltip = tooltipText.length > 0
return (
<Tooltip popupContent={tooltipText} delay={0}>
<PremiumBadge color="blue" allowHover={true}>
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-[1px] pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<div className="px-1 system-xs-medium">
{upgradeText}
</div>
</PremiumBadge>
<Tooltip>
<TooltipTrigger
delay={0}
disabled={!canShowUpgradeTooltip}
render={(
<PremiumBadge color="blue" allowHover={true}>
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-[1px] pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<div className="px-1 system-xs-medium">
{upgradeText}
</div>
</PremiumBadge>
)}
/>
{canShowUpgradeTooltip && (
<TooltipContent>
{tooltipText}
</TooltipContent>
)}
</Tooltip>
)
}