fix(ui): every full-screen overlay hosts its own toasts, above the players' controls, without re-announcing them
Deep-review round 3 of bug hunt 2026-09 (the round-2 overlay toast host, finished). - Inside an overlay the toast stack sits above the bottom band (bottom-24, the UpNextCard rung) and lets clicks between cards through: at the app's own spot it covered the Plex transport row and PlayerModal's docked bar and took their clicks while a toast was up (measured: play/pause, ±10, stop, prev/next, mute all hit the toast). - ui/Modal and the onboarding wizard mount an overlay host too, so a toast raised under any dialog is readable and a click on it no longer closes the dialog; the Modal's Tab trap spans the whole dialog, so a toast's action and dismiss are keyboard-reachable. A test now enforces the convention: a component that covers the viewport at z-overlay must mount <Toaster overlay />. - PlayerModal's host moves into the stage in true fullscreen (outside the element holding the lock it was painted under it). - A host that takes over shows the toasts it inherits silently (no second role=alert, its region aria-live=off until they are in), and each countdown bar starts as far in as its toast already is instead of restarting while the real dismiss timer does not. - The overlay-host registry uses lib/store instead of a hand-rolled listener triad.
This commit is contained in:
@@ -270,4 +270,23 @@ describe("PlayerModal — toasts raised while it is open", () => {
|
||||
act(() => void notify({ level: "error", message: "Couldn't remove the video." }));
|
||||
expect(screen.getByTestId("player-modal").textContent).toContain("Couldn't remove the video.");
|
||||
});
|
||||
|
||||
it("shows them inside the stage in true fullscreen — the element holding the lock", async () => {
|
||||
// Anything outside the fullscreen element is painted under it. The player's host sat beside the
|
||||
// card, so in fullscreen a lost connection was reported to nobody.
|
||||
vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {}));
|
||||
const A = vid("A", 0);
|
||||
renderModal(
|
||||
<PlayerModal video={A} queue={[A]} startIndex={0} onClose={() => {}} onState={() => {}} />,
|
||||
);
|
||||
const stage = document.querySelector(".player-stage")!;
|
||||
Object.defineProperty(document, "fullscreenElement", { configurable: true, get: () => stage });
|
||||
try {
|
||||
act(() => void document.dispatchEvent(new Event("fullscreenchange")));
|
||||
act(() => void notify({ level: "error", message: "Connection lost" }));
|
||||
await waitFor(() => expect(stage.textContent).toContain("Connection lost"));
|
||||
} finally {
|
||||
delete (document as { fullscreenElement?: unknown }).fullscreenElement;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user