Unify button tooltips across the app (#972)

* feat(tooltip): 2s open delay and shared tooltipAttrs helper

Add a 2s hover open delay in TooltipPortal (single behaviour source) so
tooltips no longer flash on quick pointer passes; hiding stays immediate.
Add tooltipAttrs() to pair data-tooltip with a matching aria-label for
buttons touched in the unification work. Covered by Vitest.

* feat(tooltip): lower open delay to 1s

2s felt too long in testing; 1s gives the same anti-flash behaviour
without making intentional hovers wait.

* feat(tooltip): action tooltips on the artist overview

Add tooltips describing the action to Last.fm, Wikipedia, Play All,
Shuffle and Radio. Shuffle/Radio now show a tooltip on desktop too,
not just mobile. Strings added to all 9 locales.

* feat(tooltip): action tooltips on the album overview

Add tooltips describing the action to the desktop Play, Artist Bio and
Download (ZIP) buttons, matching the mobile layout. Strings added to all
9 locales.

* feat(tooltip): action tooltips on the All Albums toolbar

Add tooltips describing the action to the sort, year and genre filter
buttons. SortDropdown gains an optional tooltip prop; the year and genre
filter components carry their own, so the tooltips also appear on the
other browse pages that reuse them. Strings added to all 9 locales.

* feat(tooltip): action tooltips on song-list rows

Add Play and Add-to-queue tooltips to the per-row icons in SongRow, used
by the Tracks browse list, Search and Advanced Search. Also localizes the
aria-labels, which were hardcoded English. New common.addToQueue in all 9
locales.

* fix(tooltip): uniform tooltip placement on the Artists toolbar

The favourite and multi-select buttons forced tooltips below while the
view-mode buttons auto-flipped above, so the row looked inconsistent.
Pin the view-mode buttons below too, matching the rest of the row and the
Albums toolbar.

* feat(tooltip): clarify and align the Advanced Search scope row

Add a leading "Search in:" label and per-chip tooltips so the
All/Artists/Albums/Songs row reads as a scope limiter. Drop the forced
below-placement on the small star filter (used only here) so the
favourites chip flips with the others instead of sitting alone below.
Strings added to all 9 locales.

* docs(changelog): tooltip unification (#972)
This commit is contained in:
Frank Stellmacher
2026-06-04 00:53:22 +02:00
committed by GitHub
parent 82c414d7bc
commit c119a32277
60 changed files with 404 additions and 34 deletions
+69
View File
@@ -0,0 +1,69 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { act, fireEvent, screen } from '@testing-library/react';
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import TooltipPortal from './TooltipPortal';
function Fixture() {
return (
<>
<TooltipPortal />
<button data-tooltip="Play this album">play</button>
</>
);
}
describe('TooltipPortal open delay', () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => {
vi.runOnlyPendingTimers();
vi.useRealTimers();
});
it('shows the tooltip only after the 1s open delay', () => {
renderWithProviders(<Fixture />);
const btn = screen.getByText('play');
fireEvent.mouseOver(btn);
expect(screen.queryByText('Play this album')).toBeNull();
act(() => {
vi.advanceTimersByTime(999);
});
expect(screen.queryByText('Play this album')).toBeNull();
act(() => {
vi.advanceTimersByTime(1);
});
expect(screen.getByText('Play this album')).toBeInTheDocument();
});
it('cancels the pending tooltip when the pointer leaves before the delay', () => {
renderWithProviders(<Fixture />);
const btn = screen.getByText('play');
fireEvent.mouseOver(btn);
act(() => {
vi.advanceTimersByTime(500);
});
fireEvent.mouseOut(btn, { relatedTarget: document.body });
act(() => {
vi.advanceTimersByTime(1000);
});
expect(screen.queryByText('Play this album')).toBeNull();
});
it('hides immediately on mousedown', () => {
renderWithProviders(<Fixture />);
const btn = screen.getByText('play');
fireEvent.mouseOver(btn);
act(() => {
vi.advanceTimersByTime(1000);
});
expect(screen.getByText('Play this album')).toBeInTheDocument();
fireEvent.mouseDown(btn);
expect(screen.queryByText('Play this album')).toBeNull();
});
});