fix: stabilize radix overlay presence

This commit is contained in:
lucaronin
2026-04-29 07:45:02 +02:00
parent ec2e04a464
commit 434927160c
3 changed files with 94 additions and 25 deletions

View File

@@ -0,0 +1,60 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from './tooltip'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from './popover'
const PRESENCE_ANIMATION_CLASS_PARTS = [
'animate-',
'fade-',
'zoom-',
'slide-in-from',
]
function expectNoPresenceAnimationClasses(element: HTMLElement) {
const unstableClasses = element.className
.split(/\s+/)
.filter((className) =>
PRESENCE_ANIMATION_CLASS_PARTS.some((part) => className.includes(part)),
)
expect(unstableClasses).toEqual([])
}
describe('overlay presence stability', () => {
it('keeps tooltip content free of Radix presence animation classes', () => {
render(
<TooltipProvider>
<Tooltip open>
<TooltipTrigger asChild>
<button type="button">Tooltip trigger</button>
</TooltipTrigger>
<TooltipContent data-testid="tooltip-content">Tooltip copy</TooltipContent>
</Tooltip>
</TooltipProvider>,
)
expectNoPresenceAnimationClasses(screen.getByTestId('tooltip-content'))
})
it('keeps popover content free of Radix presence animation classes', () => {
render(
<Popover open>
<PopoverTrigger asChild>
<button type="button">Popover trigger</button>
</PopoverTrigger>
<PopoverContent data-testid="popover-content">Popover copy</PopoverContent>
</Popover>,
)
expectNoPresenceAnimationClasses(screen.getByTestId('popover-content'))
})
})

View File

@@ -30,7 +30,7 @@ function PopoverContent({
sideOffset={sideOffset}
collisionPadding={collisionPadding}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[12000] w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
"bg-popover text-popover-foreground z-[12000] w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
@@ -45,39 +45,48 @@ function PopoverAnchor({
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="popover-header"
className={cn("flex flex-col gap-1 text-sm", className)}
{...props}
/>
)
interface PopoverTextSlotProps extends React.HTMLAttributes<HTMLElement> {
baseClassName: string
element: "div" | "p"
slot: string
}
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
return (
<div
data-slot="popover-title"
className={cn("font-medium", className)}
{...props}
/>
)
}
function PopoverDescription({
function PopoverTextSlot({
baseClassName,
className,
element,
slot,
...props
}: React.ComponentProps<"p">) {
}: PopoverTextSlotProps) {
return React.createElement(
element,
{
"data-slot": slot,
className: cn(baseClassName, className),
...props,
},
)
}
function PopoverHeader(props: React.ComponentProps<"div">) {
return (
<p
data-slot="popover-description"
className={cn("text-muted-foreground", className)}
<PopoverTextSlot
baseClassName="flex flex-col gap-1 text-sm"
element="div"
slot="popover-header"
{...props}
/>
)
}
function PopoverTitle(props: React.ComponentProps<"div">) {
return <PopoverTextSlot baseClassName="font-medium" element="div" slot="popover-title" {...props} />
}
function PopoverDescription(props: React.ComponentProps<"p">) {
return <PopoverTextSlot baseClassName="text-muted-foreground" element="p" slot="popover-description" {...props} />
}
export {
Popover,
PopoverTrigger,

View File

@@ -42,7 +42,7 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
"bg-foreground text-background z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}