fix(ui): preserve tooltip positioning under zoom
This commit is contained in:
@@ -307,6 +307,24 @@ describe('BreadcrumbBar — action buttons always right-aligned', () => {
|
||||
expect(widthActionGroup).toHaveClass('gap-2')
|
||||
})
|
||||
|
||||
it('end-aligns toolbar action tooltips so zoomed windows keep them inside the right edge', async () => {
|
||||
render(
|
||||
<BreadcrumbBar
|
||||
entry={baseEntry}
|
||||
{...defaultProps}
|
||||
onToggleFavorite={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
act(() => {
|
||||
fireEvent.focus(screen.getByRole('button', { name: 'Add to favorites' }))
|
||||
})
|
||||
|
||||
const tooltip = await screen.findByRole('tooltip')
|
||||
expect(document.querySelector('[data-slot="tooltip-content"]')).toHaveAttribute('data-align', 'end')
|
||||
expect(tooltip).toHaveTextContent('Add to favorites')
|
||||
})
|
||||
|
||||
it('lets the title use the free space before the fixed drag gap', () => {
|
||||
const { container } = render(<BreadcrumbBar entry={baseEntry} {...defaultProps} />)
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ function IconActionButton({
|
||||
style,
|
||||
children,
|
||||
testId,
|
||||
tooltipAlign,
|
||||
tooltipAlign = 'end',
|
||||
}: {
|
||||
copy: ActionTooltipCopy
|
||||
onClick?: () => void
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { act, render, renderHook, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from './tooltip'
|
||||
import { useZoom } from '@/hooks/useZoom'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -15,7 +16,8 @@ import {
|
||||
const PRESENCE_ANIMATION_CLASS_PARTS = [
|
||||
'animate-',
|
||||
'fade-',
|
||||
'zoom-',
|
||||
'zoom-in-',
|
||||
'zoom-out-',
|
||||
'slide-in-from',
|
||||
]
|
||||
|
||||
@@ -30,6 +32,11 @@ function expectNoPresenceAnimationClasses(element: HTMLElement) {
|
||||
}
|
||||
|
||||
describe('overlay presence stability', () => {
|
||||
beforeEach(() => {
|
||||
document.documentElement.style.removeProperty('--tolaria-overlay-zoom-factor')
|
||||
document.documentElement.style.removeProperty('--tolaria-overlay-zoom-inverse')
|
||||
})
|
||||
|
||||
it('keeps tooltip content free of Radix presence animation classes', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
@@ -45,6 +52,38 @@ describe('overlay presence stability', () => {
|
||||
expectNoPresenceAnimationClasses(screen.getByTestId('tooltip-content'))
|
||||
})
|
||||
|
||||
it('publishes zoom variables for overlay portal positioning and visual scale', () => {
|
||||
const { result } = renderHook(() => useZoom())
|
||||
|
||||
act(() => {
|
||||
result.current.zoomIn()
|
||||
})
|
||||
|
||||
expect(document.documentElement.style.getPropertyValue('--tolaria-overlay-zoom-factor')).toBe(String(110 / 100))
|
||||
expect(document.documentElement.style.getPropertyValue('--tolaria-overlay-zoom-inverse')).toBe(String(100 / 110))
|
||||
})
|
||||
|
||||
it('compensates tooltip portal positioning without cancelling content zoom', () => {
|
||||
document.documentElement.style.setProperty('--tolaria-overlay-zoom-factor', '1.4')
|
||||
document.documentElement.style.setProperty('--tolaria-overlay-zoom-inverse', String(1 / 1.4))
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip open>
|
||||
<TooltipTrigger asChild>
|
||||
<button type="button">Tooltip trigger</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent data-testid="tooltip-content">Tooltip copy</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const positionShell = document.querySelector('[data-slot="tooltip-content"]') as HTMLElement
|
||||
const visualShell = document.querySelector('[data-slot="tooltip-visual-scale"]') as HTMLElement
|
||||
expect(positionShell.className).toContain('[zoom:var(--tolaria-overlay-zoom-inverse,1)]')
|
||||
expect(visualShell.className).toContain('[zoom:var(--tolaria-overlay-zoom-factor,1)]')
|
||||
})
|
||||
|
||||
it('keeps popover content free of Radix presence animation classes', () => {
|
||||
render(
|
||||
<Popover open>
|
||||
|
||||
@@ -33,7 +33,9 @@ function TooltipTrigger({
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
collisionPadding = 8,
|
||||
children,
|
||||
style,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
@@ -41,14 +43,23 @@ function TooltipContent({
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
collisionPadding={collisionPadding}
|
||||
className={cn(
|
||||
"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
|
||||
"z-50 w-fit max-w-[min(var(--radix-tooltip-content-available-width,22rem),22rem)] origin-(--radix-tooltip-content-transform-origin) [zoom:var(--tolaria-overlay-zoom-inverse,1)]"
|
||||
)}
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
<div
|
||||
data-slot="tooltip-visual-scale"
|
||||
className={cn(
|
||||
"bg-foreground text-background max-w-[inherit] rounded-md px-3 py-1.5 text-xs text-balance [zoom:var(--tolaria-overlay-zoom-factor,1)]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</div>
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
|
||||
@@ -28,6 +28,8 @@ function loadPersistedZoom(): number {
|
||||
|
||||
function applyZoomToDocument(level: number): void {
|
||||
document.documentElement.style.setProperty('zoom', `${level}%`)
|
||||
document.documentElement.style.setProperty('--tolaria-overlay-zoom-factor', String(level / DEFAULT_ZOOM))
|
||||
document.documentElement.style.setProperty('--tolaria-overlay-zoom-inverse', String(DEFAULT_ZOOM / level))
|
||||
window.dispatchEvent(new Event('laputa-zoom-change'))
|
||||
}
|
||||
|
||||
@@ -40,7 +42,7 @@ export function useZoom() {
|
||||
const level = loadPersistedZoom()
|
||||
// Apply zoom synchronously during init so child components (e.g. CodeMirror)
|
||||
// measure the correct scale factor in their own effects.
|
||||
document.documentElement.style.setProperty('zoom', `${level}%`)
|
||||
applyZoomToDocument(level)
|
||||
return level
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user