diff --git a/.changeset/every-balloons-open.md b/.changeset/every-balloons-open.md new file mode 100644 index 0000000000..064ed11338 --- /dev/null +++ b/.changeset/every-balloons-open.md @@ -0,0 +1,5 @@ +--- +"@digdir/designsystemet-web": patch +--- + +**tooltip**: Fixes a bug where tooltip would reappear on mousedown outside trigger diff --git a/packages/web/src/tooltip/tooltip.test.ts b/packages/web/src/tooltip/tooltip.test.ts index 2350c90ac2..9f1c77c154 100644 --- a/packages/web/src/tooltip/tooltip.test.ts +++ b/packages/web/src/tooltip/tooltip.test.ts @@ -96,6 +96,62 @@ describe('tooltip behavior', () => { expect(tip.hidePopover).toHaveBeenCalledTimes(1); }); + it('hides tooltip on blur when focus leaves to a non-tooltip element', async () => { + const tip = document.createElement('div'); + tip.showPopover = vi.fn(); + tip.hidePopover = vi.fn(); + setTooltipElement(tip); + + document.body.innerHTML = ``; + + const button = document.querySelector('button') as HTMLButtonElement; + button.dispatchEvent(new FocusEvent('focus')); + expect(tip.showPopover).toHaveBeenCalledTimes(1); + + button.dispatchEvent(new FocusEvent('blur')); + expect(tip.hidePopover).toHaveBeenCalledTimes(1); + expect(tip.showPopover).toHaveBeenCalledTimes(1); // Must not reshow on blur + }); + + it('does not reshow tooltip on blur (regression #4801)', async () => { + const tip = document.createElement('div'); + tip.showPopover = vi.fn(); + tip.hidePopover = vi.fn(); + setTooltipElement(tip); + + document.body.innerHTML = ``; + + const button = document.querySelector('button') as HTMLButtonElement; + button.dispatchEvent(new FocusEvent('focus')); + expect(tip.showPopover).toHaveBeenCalledTimes(1); + + // Reset internal source by hiding (mimics moving mouse away/closing) + setTooltipElement(tip); + + // Blurring the still-focused button (e.g. clicking elsewhere) must not reshow + button.dispatchEvent(new FocusEvent('blur')); + expect(tip.showPopover).toHaveBeenCalledTimes(1); + }); + + it('does not reshow the previous tooltip when blurring to another tooltip trigger', async () => { + const tip = document.createElement('div'); + tip.showPopover = vi.fn(); + tip.hidePopover = vi.fn(); + setTooltipElement(tip); + + document.body.innerHTML = ``; + + const a = document.querySelector('#a') as HTMLButtonElement; + const b = document.querySelector('#b') as HTMLButtonElement; + a.dispatchEvent(new FocusEvent('focus')); + expect(tip.showPopover).toHaveBeenCalledTimes(1); + + // Focus moves to another tooltip trigger: blur must not hide nor reshow + a.dispatchEvent(new FocusEvent('blur', { relatedTarget: b })); + expect(tip.hidePopover).not.toHaveBeenCalled(); + expect(tip.showPopover).toHaveBeenCalledTimes(1); + }); + it('updates tooltip text and announces when data-tooltip changes programmatically', async () => { const tip = document.createElement('div'); setTooltipElement(tip); diff --git a/packages/web/src/tooltip/tooltip.ts b/packages/web/src/tooltip/tooltip.ts index 3e03b5eaf3..df7d6184b9 100644 --- a/packages/web/src/tooltip/tooltip.ts +++ b/packages/web/src/tooltip/tooltip.ts @@ -69,16 +69,26 @@ const handleAriaAttributes = () => { } }; -const handleInterest = ({ type, target }: Event) => { +const handleInterest = (event: Event) => { + const { type, target } = event; clearTimeout(HOVER_TIMER); if (target === TIP) return; // Allow tooltip to be hovered, following https://www.w3.org/TR/WCAG21/#content-on-hover-or-focus + + const source = (target as Element)?.closest?.(SELECTOR_TOOLTIP); + + // This prevents the tooltip from reappearing on mousedown/click + if (type === 'blur') { + const next = (event as FocusEvent).relatedTarget as Element | null; + if (source === SOURCE && !next?.closest?.(SELECTOR_TOOLTIP)) hideTooltip(); + return; + } + if (type === 'mouseover' && !IS_HOVERING && !IS_IOS) { HOVER_TIMER = setTimeout(handleInterest, DELAY_HOVER, { target }); // Delay mouse showing tooltip if not already shown return; } - const source = (target as Element)?.closest?.(`[${ATTR_TOOLTIP}]`); if (source === SOURCE) return; // No need to update if (!source) return hideTooltip(); // If no new anchor, cleanup previous autoUpdate if (!TIP) TIP = tag('div', { class: 'ds-tooltip' });