fix(ui): a card's hover chrome answers keyboard focus, not any focus
Back from a channel page hands the card its focus again after a mouse trip; revealed on focus-within, its hover row stayed up under no pointer until a reload (owner). The row, the Play layer, the Continue + Restart pair and the global .reveal rule now show on hover or on :focus-visible on the card or inside it, so Tab and the arrow keys still reveal it.
This commit is contained in:
@@ -295,7 +295,7 @@ function Thumb({
|
||||
</span>
|
||||
)}
|
||||
{/* Hover overlay: Play everywhere; Continue + Restart once the video is in progress. Mouse-only:
|
||||
reveal on hover AND on keyboard focus-within (else a Tab-focused button is invisible), and
|
||||
reveal on hover AND on keyboard focus (else a Tab-focused button is invisible), and
|
||||
`reveal-hover` drops it entirely on touch — the card tap already plays/resumes there, so a
|
||||
permanent dark scrim over every mobile thumbnail (and an invisible tap target) is not wanted. */}
|
||||
{onOpen && (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import videoCard from "./VideoCard.tsx?raw";
|
||||
import { HOVER_REVEAL, HOVER_YIELD } from "./ui/reveal";
|
||||
|
||||
import {
|
||||
MEDIA_BAR,
|
||||
@@ -38,3 +40,28 @@ describe("a media picture", () => {
|
||||
expect(layer).not.toMatch(/pic:pb-\d/);
|
||||
});
|
||||
});
|
||||
|
||||
const indexCss = readFileSync(new URL("../index.css", import.meta.url), "utf8");
|
||||
|
||||
// Back from a channel page hands the card its focus again, after a mouse trip: with `focus-within`
|
||||
// its hover row stayed up under no pointer until a reload (owner). The chrome answers the pointer and
|
||||
// KEYBOARD focus only (`:focus-visible` on the card or inside it); jsdom cannot evaluate either, so
|
||||
// the classes are the contract, and the global `.reveal` rule says the same.
|
||||
describe("a card's hover chrome", () => {
|
||||
it.each([
|
||||
["the hover row", HOVER_REVEAL],
|
||||
["what yields to it", HOVER_YIELD],
|
||||
["the Play layer", MEDIA_HOVER_LAYER],
|
||||
["the Continue + Restart layer", MEDIA_RESUME_LAYER],
|
||||
])("shows on keyboard focus, never on a focus handed back: %s", (_, cls) => {
|
||||
const parts = cls.split(" ");
|
||||
expect(parts.some((c) => c.startsWith("group-focus-within:"))).toBe(false);
|
||||
expect(parts.some((c) => c.startsWith("group-focus-visible:"))).toBe(true);
|
||||
expect(parts.some((c) => c.startsWith("group-has-focus-visible:"))).toBe(true);
|
||||
});
|
||||
|
||||
it("is the same in the global .reveal rule", () => {
|
||||
expect(indexCss).not.toMatch(/\.group:focus-within \.reveal/);
|
||||
expect(indexCss).toMatch(/\.group:has\(:focus-visible\) \.reveal/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ export const MEDIA_PICTURE_CONTAINER = "@container/pic has-[.media-bar]:[--bar-r
|
||||
* (measured). The cut is in rem, so it moves with the text size too: a picture under 14rem wide.
|
||||
* Only where a row is there to clear (`--bar-room`, MEDIA_PICTURE_CONTAINER). */
|
||||
export const MEDIA_HOVER_LAYER =
|
||||
"reveal-hover absolute inset-0 flex items-center justify-center gap-2 bg-black/45 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 @max-[14rem]/pic:pb-(--bar-room)";
|
||||
"reveal-hover absolute inset-0 flex items-center justify-center gap-2 bg-black/45 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100 group-has-focus-visible:opacity-100 @max-[14rem]/pic:pb-(--bar-room)";
|
||||
|
||||
/** The layer that holds a started title's Continue + Restart pair over the picture: a sibling of the
|
||||
* picture's button, never inside it (C5). Narrow, it leaves the hover row its band as the Play does. */
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// A card's hover chrome: shown when the card is hovered or holds keyboard focus on a mouse,
|
||||
// A card's hover chrome: shown when the card is hovered or holds KEYBOARD focus on a mouse,
|
||||
// removed on touch (`.reveal-hover`, index.css), where the card offers the same actions through its
|
||||
// "…" beside the title. One string, so the video card and the Plex cards reveal the same way.
|
||||
// Keyboard focus is `:focus-visible` on the card or inside it, not any focus: Back from a channel
|
||||
// page hands the card its focus back, and with `:focus-within` the row stayed up under no pointer
|
||||
// until a reload (owner).
|
||||
export const HOVER_REVEAL =
|
||||
"reveal-hover opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity";
|
||||
"reveal-hover opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 group-has-focus-visible:opacity-100 transition-opacity";
|
||||
|
||||
/** What sits under that chrome at rest (a duration, a watched pill) steps aside while it shows. */
|
||||
export const HOVER_YIELD = "transition-opacity group-hover:opacity-0 group-focus-within:opacity-0";
|
||||
export const HOVER_YIELD =
|
||||
"transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0 group-has-focus-visible:opacity-0";
|
||||
|
||||
Reference in New Issue
Block a user