feat(a11y): touch-visible card controls + modal focus trap/restore (E4 S6)
UX-7 — hover-only card actions were `opacity-0 group-hover:opacity-100`: fine on a mouse, but invisible AND still tappable on a touch device (no hover). New `.reveal` primitive (index.css) gates the hide on `@media (hover: hover)`, so a touch device keeps the control visible; its partner `.reveal-idle` is the idle badge a control replaces (watched/in-progress pill under the watched-toggle), faded on hover and dropped on touch so the two don't collide at the same corner. Migrated the true controls — feed card action toolbar (VideoCard), Plex poster card watched-toggle/info/add across all card variants + their swap badges (plexCards), message pop-out (Messages), edit-interval pencil (Scheduler), and saved-view star/rename/delete (SavedViewsWidget). Pure play-scrim overlays (the whole card is the tap target) stay hover-only by design. FE-9 — Modal.tsx had aria-modal but no focus management. Adds: a Tab/Shift+Tab focus trap (topmost layer only, wraps at the edges and pulls escaped focus back); initial focus into the dialog unless an autoFocus child already placed it; focus restore to the trigger on close (captured in a useState initializer during render, before commit runs autoFocus and moves activeElement inside); and aria-labelledby wiring the dialog to its title. Gate green (tsc, knip, lint 0-err, 247 vitest). Self-E2E (localdev): FE-9 trap both directions + pull-back + Escape-restore-to-trigger + aria-labelledby verified; UX-7 feed toolbar opacity 0 on desktop / 1 under mobile (hover:none) emulation, Plex reveal buttons opacity 1 + reveal-idle badges display:none on touch.
This commit is contained in:
@@ -536,6 +536,38 @@ body {
|
||||
0 14px 30px -14px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* UX-7 — touch-visible reveal for hover-only card controls. A control tagged `.reveal` used to be
|
||||
`opacity-0 group-hover:opacity-100`: fine on a mouse (the enclosing `.group` reveals it on hover),
|
||||
but on a touch device — which never hovers — it is invisible AND still tappable. So the reveal is
|
||||
gated on `@media (hover: hover)`: only a hover-capable pointer hides it until the group is hovered
|
||||
or focused (keyboard too, via `:focus-within`); a touch device keeps the default opacity:1 and the
|
||||
control is always shown. `.reveal-idle` is the counterpart badge a control replaces (e.g. the
|
||||
"watched" pill under the watched-toggle): visible at rest, faded when the group is hovered, and
|
||||
dropped entirely on touch — where the toggle is always present, so the two would otherwise collide
|
||||
at the same corner. Requires a `.group` ancestor (Tailwind's group marker). */
|
||||
.reveal-idle {
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
.group:hover .reveal,
|
||||
.group:focus-within .reveal,
|
||||
.reveal:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
.group:hover .reveal-idle {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@media (hover: none) {
|
||||
.reveal-idle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode: the ambient accent pools are faint over a near-black bg, so the docked frosted
|
||||
chrome (nav/header) barely reads — there's no content behind it to refract, unlike dialogs
|
||||
which sit over the feed. Strengthen the pools and let a touch more bleed through the glass.
|
||||
|
||||
Reference in New Issue
Block a user