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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -70,24 +70,36 @@ export function useFullscreen(wrapRef: RefObject<HTMLElement | null>): {
|
||||
// 3s hide timer. Reads the caller's live refs so the timer callback sees fresh state without a
|
||||
// closure rebuild: barAutoHide (per-user "never auto-hide" setting), whether a menu is open (keep the
|
||||
// bar up so it can't vanish mid-choice), and the <video> paused state (a paused video keeps the bar).
|
||||
// `holdOnHover` is the pointer resting on the chrome: the bar stays while it is there (as on YouTube
|
||||
// and Plex Web — E28 F22, it used to fade out from under a mouse parked on Next), and leaving it
|
||||
// starts a fresh 3 s.
|
||||
export function useControlsVisibility(opts: {
|
||||
barAutoHideRef: RefObject<boolean>;
|
||||
menuOpenRef: RefObject<boolean>;
|
||||
videoRef: RefObject<HTMLVideoElement | null>;
|
||||
}): { uiVisible: boolean; wake: () => void } {
|
||||
}): { uiVisible: boolean; wake: () => void; holdOnHover: (over: boolean) => void } {
|
||||
const { barAutoHideRef, menuOpenRef, videoRef } = opts;
|
||||
const [uiVisible, setUiVisible] = useState(true);
|
||||
const hideTimer = useRef<number | null>(null);
|
||||
const hovered = useRef(false);
|
||||
const wake = useCallback(() => {
|
||||
setUiVisible(true);
|
||||
if (hideTimer.current) window.clearTimeout(hideTimer.current);
|
||||
if (!barAutoHideRef.current) return; // per-user setting: never auto-hide the control bar
|
||||
hideTimer.current = window.setTimeout(() => {
|
||||
// Keep the controls up while the audio/subtitle menu is open, else it would vanish mid-choice.
|
||||
if (!videoRef.current?.paused && !menuOpenRef.current) setUiVisible(false);
|
||||
if (!videoRef.current?.paused && !menuOpenRef.current && !hovered.current)
|
||||
setUiVisible(false);
|
||||
}, 3000);
|
||||
}, [barAutoHideRef, menuOpenRef, videoRef]);
|
||||
return { uiVisible, wake };
|
||||
const holdOnHover = useCallback(
|
||||
(over: boolean) => {
|
||||
hovered.current = over;
|
||||
if (!over) wake();
|
||||
},
|
||||
[wake],
|
||||
);
|
||||
return { uiVisible, wake, holdOnHover };
|
||||
}
|
||||
|
||||
// Periodic + on-unmount progress checkpoint. Reads live position/duration through refs (owned by the
|
||||
|
||||
@@ -74,7 +74,6 @@ export const DEFAULT_PREFS: PlexPlayerPrefs = {
|
||||
autoSkipDelay: 5,
|
||||
upNext: true,
|
||||
};
|
||||
export const clamp01 = (n: number) => Math.max(0, Math.min(1, n));
|
||||
|
||||
// A punchy subtitle shadow. The old single 0-offset blur washed out to nothing on bright scenes even
|
||||
// at the max slider value; instead lay down eight offset copies (a solid dark OUTLINE around the
|
||||
|
||||
Reference in New Issue
Block a user