perf(ui-trajectory): defer trajectory text processing
This commit is contained in:
@@ -24,9 +24,11 @@ interface AnchorProps {
|
||||
onBlur?: FocusEventHandler | undefined
|
||||
}
|
||||
|
||||
type TooltipLabel = string | (() => string)
|
||||
|
||||
/**
|
||||
* Attach a hover/focus tooltip to an anchor element.
|
||||
* @param props.label - bubble text.
|
||||
* @param props.label - bubble text, or a resolver evaluated only while the bubble is visible.
|
||||
* @param props.side - placement relative to the anchor (default 'right').
|
||||
* @param props.delayMs - hover delay in milliseconds; keyboard focus remains immediate.
|
||||
* @param props.disabled - suppress the bubble while true; the anchor renders identically so
|
||||
@@ -34,7 +36,7 @@ interface AnchorProps {
|
||||
* @param props.children - a single anchor element; its own ref (callback or object) is forwarded alongside the tooltip's.
|
||||
* @returns the cloned anchor plus a fixed-position bubble while hovered/focused.
|
||||
*/
|
||||
export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false, children }: { label: string; side?: TooltipSide; delayMs?: number; disabled?: boolean; children: ReactElement<AnchorProps> }) {
|
||||
export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false, children }: { label: TooltipLabel; side?: TooltipSide; delayMs?: number; disabled?: boolean; children: ReactElement<AnchorProps> }) {
|
||||
const anchor = useRef<HTMLElement | null>(null)
|
||||
// React 18 keeps the element's ref outside props; forward it so wrapping an
|
||||
// anchor in Tooltip never silently severs the owner's ref.
|
||||
@@ -46,6 +48,9 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
}, [childRef])
|
||||
const [pos, setPos] = useState<{ x: number; y: number } | null>(null)
|
||||
const bubble = useRef<HTMLSpanElement | null>(null)
|
||||
const resolvedLabel = pos === null
|
||||
? null
|
||||
: typeof label === 'function' ? label() : label
|
||||
// Horizontal viewport clamp: fixed positioning knows nothing about edges, so
|
||||
// a centered bubble near the right edge would clip. Each measurement resets
|
||||
// the base position before applying a direct style offset, allowing a shorter
|
||||
@@ -67,7 +72,7 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
clamp()
|
||||
window.addEventListener('resize', clamp)
|
||||
return () => { window.removeEventListener('resize', clamp) }
|
||||
}, [label, pos])
|
||||
}, [pos, resolvedLabel])
|
||||
const showTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
// Hover and focus are independent triggers: the bubble hides only after
|
||||
// BOTH clear (hovering away from a focused anchor must not drop it).
|
||||
@@ -128,7 +133,7 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
})}
|
||||
{pos !== null && (
|
||||
<span ref={bubble} className={css.bubble} data-side={side} style={{ left: pos.x, top: pos.y }} role="tooltip">
|
||||
{label}
|
||||
{resolvedLabel}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -6,6 +6,27 @@ import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('Tooltip', () => {
|
||||
it('resolves lazy labels only after the bubble becomes visible', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const label = vi.fn(() => 'Timing details')
|
||||
render(
|
||||
<Tooltip label={label} delayMs={500}>
|
||||
<button type="button">anchor</button>
|
||||
</Tooltip>,
|
||||
)
|
||||
expect(label).not.toHaveBeenCalled()
|
||||
fireEvent.mouseEnter(screen.getByText('anchor'))
|
||||
act(() => { vi.advanceTimersByTime(499) })
|
||||
expect(label).not.toHaveBeenCalled()
|
||||
act(() => { vi.advanceTimersByTime(1) })
|
||||
expect(screen.getByRole('tooltip').textContent).toBe('Timing details')
|
||||
expect(label).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('can delay pointer hover without delaying keyboard focus', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user