fix(player): Escape coordination, focused-Space, light-theme menus (U-C, U-3.1.4, U-3.3.12)
Three player/overlay interaction fixes (PlexPlayer changes share a file, hence one commit): - U-C (Escape double-close): useDismiss's Escape now stopPropagation()s (its document listener bubbles before a player's window listener, so one Escape closes only the popover, not the popover AND the player). Modal exports modalCount(); both players' Escape defers when a Modal is open above them, so Escape closes the dialog, not the player under it and then the dialog. - U-3.1.4: PlexPlayer's Space now defers to a focused BUTTON/A (e.g. "Skip intro") instead of toggling playback — matching PlayerModal. - U-3.3.12: the audio/subtitle menus used theme-adaptive glass-menu + forced text-white (white-on-white in light theme); they now use the player's own explicit dark panel (border-white/15 + bg-neutral-900/95), readable in both themes.
This commit is contained in:
@@ -8,6 +8,13 @@ import { useBackToClose } from "../lib/history";
|
||||
let modalStack: number[] = [];
|
||||
let nextModalId = 1;
|
||||
|
||||
/** How many <Modal>s are currently open. The players (which use their own window-level Escape
|
||||
* listeners, not this stack) check it so their Escape defers to a Modal opened above them —
|
||||
* otherwise one Escape closes the player UNDER the dialog and then the dialog too (U-C). */
|
||||
export function modalCount(): number {
|
||||
return modalStack.length;
|
||||
}
|
||||
|
||||
// Small centered modal shell (portaled to <body>): backdrop + ESC + scroll-lock close.
|
||||
export default function Modal({
|
||||
title,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import Avatar from "./Avatar";
|
||||
import AddToPlaylist from "./AddToPlaylist";
|
||||
import { modalCount } from "./Modal";
|
||||
import DownloadButton from "./DownloadButton";
|
||||
import { api, type Video } from "../lib/api";
|
||||
import {
|
||||
@@ -375,6 +376,9 @@ export default function PlayerModal({
|
||||
if (e.key === "Escape") {
|
||||
// In fullscreen, let the browser's Esc exit fullscreen only — don't also close the modal.
|
||||
if (document.fullscreenElement) return;
|
||||
// Defer to any Modal opened above the player (e.g. the download dialog): its Escape closes
|
||||
// it, ours would otherwise also fire and close the player underneath it (U-C).
|
||||
if (modalCount() > 0) return;
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { LS, useAccountPersistedObject } from "../lib/storage";
|
||||
import { useDismiss } from "../lib/useDismiss";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
import { useBackToClose } from "../lib/history";
|
||||
import { modalCount } from "./Modal";
|
||||
|
||||
// The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page.
|
||||
const PlexInfo = lazy(() => import("./PlexInfo"));
|
||||
@@ -809,6 +810,12 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
wake(); // keyboard use also reveals the controls
|
||||
switch (e.key) {
|
||||
case " ":
|
||||
// Let Space activate a focused button/link natively (e.g. "Skip intro"); only toggle
|
||||
// playback when the focus isn't on a control — matching PlayerModal's guard.
|
||||
if (tag === "BUTTON" || tag === "A") return;
|
||||
e.preventDefault();
|
||||
togglePlay();
|
||||
break;
|
||||
case "k":
|
||||
e.preventDefault();
|
||||
togglePlay();
|
||||
@@ -861,6 +868,8 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
else handleBack();
|
||||
break;
|
||||
case "Escape":
|
||||
// Defer to any Modal opened above the player — let its Escape close it, not ours (U-C).
|
||||
if (modalCount() > 0) break;
|
||||
if (skipProgressRef.current != null) cancelAutoSkipRef.current();
|
||||
else if (menuOpenRef.current) {
|
||||
setMenuOpen(false);
|
||||
@@ -1332,7 +1341,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
style={tracksFade.style}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
className="glass-menu absolute bottom-full right-0 mb-2 w-56 max-h-[60vh] overflow-auto no-scrollbar rounded-xl p-2 text-sm text-white shadow-2xl"
|
||||
className="border border-white/15 bg-neutral-900/95 absolute bottom-full right-0 mb-2 w-56 max-h-[60vh] overflow-auto no-scrollbar rounded-xl p-2 text-sm text-white shadow-2xl"
|
||||
>
|
||||
{detail.audio_streams.length > 1 && (
|
||||
<>
|
||||
@@ -1395,7 +1404,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
ref={menuRef}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="glass-menu absolute bottom-full right-0 mb-2 w-80 rounded-xl p-2 text-sm text-white shadow-2xl"
|
||||
className="border border-white/15 bg-neutral-900/95 absolute bottom-full right-0 mb-2 w-80 rounded-xl p-2 text-sm text-white shadow-2xl"
|
||||
>
|
||||
<div className="mb-1.5 flex gap-1">
|
||||
{settingsTabs.map((tab) => (
|
||||
|
||||
@@ -17,7 +17,13 @@ export function useDismiss(
|
||||
if (!list.some((r) => r.current?.contains(target))) onClose();
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
if (e.key === "Escape") {
|
||||
// Stop the same Escape from also reaching a player's window-level listener underneath —
|
||||
// this document listener bubbles first, so one Escape closes only this popover, not the
|
||||
// popover AND the player behind it. (U-C quick-fix.)
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", onDown);
|
||||
document.addEventListener("keydown", onKey);
|
||||
|
||||
Reference in New Issue
Block a user