fix(player): the Plex bars hold while hovered, volume is the native slider, tooltips no longer re-layer the row (E28)

- F22: the control bar faded out from under a mouse parked on it; the hide timer
  now waits while the pointer is on either band and re-arms on leave (test seen
  RED by disabling the hold).
- The hand-drawn volume bar broke DESIGN.md A6 (native sliders): it had no hover,
  focus, keyboard or screen-reader state. It is the Slider primitive now; a
  pointer drag blurs it so Space and the arrows keep driving the player.
- The control tooltips are composited from mount (will-change: opacity): a fade
  that promoted one on hover re-layered its neighbours under the player's
  scale(1.25), and the whole row shimmered.
This commit is contained in:
2026-09-25 14:48:45 +02:00
parent e9ccd9bc98
commit 62dbf35ea3
8 changed files with 106 additions and 51 deletions
@@ -0,0 +1,47 @@
// @vitest-environment jsdom
//
// The control bar must not hide while the pointer rests on it. It used to: the 3 s timer only knew
// "paused" and "a menu is open", so with the mouse parked on the row (about to press Next) the bar
// faded out from under it (E28 audit F22). YouTube and Plex Web both hold the bar while hovered.
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
import { act, renderHook } from "@testing-library/react";
import { useControlsVisibility } from "./plexPlayerHooks";
const setup = () => {
const video = { paused: false } as HTMLVideoElement;
return renderHook(() =>
useControlsVisibility({
barAutoHideRef: { current: true },
menuOpenRef: { current: false },
videoRef: { current: video },
}),
);
};
describe("useControlsVisibility", () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("hides 3 s after the last activity while playing", () => {
const { result } = setup();
act(() => result.current.wake());
act(() => vi.advanceTimersByTime(3000));
expect(result.current.uiVisible).toBe(false);
});
it("stays up while the pointer is over the bar, and re-arms the timer when it leaves", () => {
const { result } = setup();
act(() => {
result.current.wake();
result.current.holdOnHover(true);
});
act(() => vi.advanceTimersByTime(10_000));
expect(result.current.uiVisible).toBe(true);
act(() => result.current.holdOnHover(false));
act(() => vi.advanceTimersByTime(2900));
expect(result.current.uiVisible).toBe(true); // a fresh 3 s from the leave, not from the wake
act(() => vi.advanceTimersByTime(100));
expect(result.current.uiVisible).toBe(false);
});
});