fix(tooltip): the hover entrance animation never played — content mounted as instant-open

The tooltip's `delayed-open` entrance (the per-side slide, and now the bounce) NEVER
fired: the content only mounts when `open` becomes true, and the hover-delay timer
set `internalTooltipState = 'instant-open'` AT THAT MOMENT — so the content always
mounted as `instant-open` (no entrance). `delayed-open` was only ever the pending
state while the content was still unmounted. A long-standing dead animation.

Fix: the hover-delay timer now keeps `'delayed-open'` as the content mounts, so the
eidos `[data-state='delayed-open']` entrance plays. `'instant-open'` stays reserved
for focus / skip-delay (intentionally no entrance). Test updated (it asserted the
buggy `instant-open`); tooltip provider suite 4/4.

Also makes the bounce clearly perceptible (it was too subtle to notice even when
forced): scale 0.5 → 1.18 (overshoot) → 0.9 (undershoot) → 1.06 (second bounce) → 1,
over `--duration-slow`. Verified end-to-end in-browser: hover → content mounts
`delayed-open` → `animation-name: eidos-tooltip-bounce-in`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 7add5cbda0
commit 3d41459936

@ -96,7 +96,7 @@
* registered `motion` preset. */
[data-tooltip-content][data-state='delayed-open'] {
animation: eidos-tooltip-bounce-in var(--duration-moderate) var(--ease-out) both;
animation: eidos-tooltip-bounce-in var(--duration-slow) var(--ease-out) both;
}
[data-tooltip-content][data-state='closed'] {
animation: eidos-tooltip-bounce-out var(--duration-fast) var(--ease-in) both;
@ -108,14 +108,15 @@
transform: none;
}
/* A real bounce: grow past 1 (overshoot 1.06), dip slightly under (undershoot
* 0.97), settle to 1. Subtle in absolute terms (a tooltip is small) but clearly a
* "rebote". `scale` (the individual property, not the `transform` shorthand)
/* A real bounce (muelle): grow from half size, overshoot clearly past 1, dip
* under, a smaller second overshoot, then settle — a perceptible spring, not a
* subtle pop. `scale` (the individual property, not the `transform` shorthand)
* composes with `transform-origin` and never clobbers a sibling transform. */
@keyframes eidos-tooltip-bounce-in {
0% { opacity: 0; scale: 0.7; }
50% { opacity: 1; scale: 1.06; }
74% { scale: 0.97; }
0% { opacity: 0; scale: 0.5; }
38% { opacity: 1; scale: 1.18; }
60% { scale: 0.9; }
80% { scale: 1.06; }
100% { scale: 1; }
}
@keyframes eidos-tooltip-bounce-out {

@ -151,8 +151,9 @@ describe('TooltipProvider', () => {
harness.tasks.get('soma:tooltip:tooltip-root:open')?.();
await tick();
expect(opts.open.current).toBe(true);
expect(result.provider.tooltipState).toBe('instant-open');
expect(triggerEl.getAttribute('data-state')).toBe('instant-open');
// Hover-delay path opens with the ENTRANCE state so the eidos animation plays.
expect(result.provider.tooltipState).toBe('delayed-open');
expect(triggerEl.getAttribute('data-state')).toBe('delayed-open');
result.trigger.onpointerleave();
expect(harness.schedule).toHaveBeenCalledWith(

@ -220,7 +220,12 @@ export class TooltipProvider {
this.delay,
() => {
this.openTimer = null;
this.internalTooltipState = 'instant-open';
// Stay 'delayed-open' (NOT 'instant-open') as the content MOUNTS, so the
// eidos entrance animation keyed on `[data-state='delayed-open']` actually
// plays. The content only mounts here (open → true), so setting
// 'instant-open' meant the hover entrance NEVER fired. 'instant-open' stays
// reserved for focus / skip-delay (no entrance).
this.internalTooltipState = 'delayed-open';
this.opts.open.current = true;
},
{ replace: true, meta: { component: 'tooltip', action: 'open-delay' } }

Loading…
Cancel
Save

Powered by TurnKey Linux.