fix(player): keyboard focus, contrast and ARIA for controls and settings (#1769)

* fix(player): keyboard focus, contrast and ARIA for controls and settings

Dock and settings-panel icon buttons draw a 2px --pc-text ring on
:focus-visible, and Material's theme-coloured focus layer is off, so
keyboard focus shows on video in the light theme too. A focused selected
subtitle swatch now differs from one that is only selected.

Settings headings read --pc-text-secondary on denser glass
(--pc-glass-bg-dense, 0.86): 4.5:1 or more over mid-grey and white
frames. They wrap (overflow-wrap: anywhere, hyphens: auto), so long
German and Russian headings stay inside the sheet's heading column.

The settings panel is now radio groups only (SettingsRadioGroupDirective
over a CDK FocusKeyManager): one Tab stop per group on the checked option,
arrows, Home and End move focus without applying, and Space/Enter checks.
The dialog and its groups are named by real h2/h3/h4 headings, the
load-file action sits outside the subtitle radio group, the subtitle and
speed chips carry their value in their name ("Subtitles: English"), and
tune has aria-haspopup="dialog".

Adds a web E2E for the keyboard path in both themes with an axe check on
the open panel, and de/ru sheet heading wrapping; axe-core is a new dev
dependency for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(player): arrows check settings radios; subtitle chip reads On

Review follow-up:
- Arrow keys, Home and End now check the settings radio they reach, as a
  native radio group does (the directive clicks it, so the template's
  handler applies the choice); an option the engine already reports as
  checked is not applied again.
- With subtitles on but no track marked selected yet (the engine can
  report the switch before the track list), the subtitle chip reads and
  announces "On" (new SUBTITLES_ON key, 19 locales) instead of "Off".
- The swatch row has 4px padding on every side, so the outer focus ring
  is not clipped at the scroll edge of the panel body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(player): re-apply a settings radio while a switch is pending

The arrow-key check skipped any option the engine still reported as
checked. Arrowing from audio track A to B and back to A before the engine
confirmed B therefore sent no command for A, and playback ended on B with
focus on A. An arrow move always lands on an option other than the last
one applied, so it now applies unconditionally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 authored and GitHub committed 2026-10-01 06:37:02 +02:00
1 parent 5d50995c30
commit adb4889b0f
37 files changed
+1450 -257

No files matched your search

+3 -1
View File
@@ -91,7 +91,9 @@ themes and fullscreen; frame-copy and built-in shared controls keep their
light-on-dark overlay palette — the fixed `--pc-*` token set of the shared
dock (accent blue, cyan, violet, the `--pc-live` / `--pc-danger` reds and a
light text ramp), never the app theme. The overlay styles in
`player-controls/` never read a `--mat-sys-*` token.
`player-controls/` never read a `--mat-sys-*` token, and their keyboard focus
is a 2px `--pc-text` outline rather than Material's theme-coloured focus layer
(`player-controls-keyboard.e2e.ts` checks it in both themes).
EPG timeline, list, empty states and programme details use the library-local
`libs/ui/epg/src/lib/_epg-theme.scss` palette, based on app surfaces, separators,
+40 -9
View File
@@ -297,7 +297,8 @@ It owns only transient presentation behavior:
- `ControlsSettings` — the settings panel's groups, on/modified state and
open/close transitions (`controls-settings-groups.ts` holds the pure
group-availability rule);
- `app-player-settings-panel` — the panel / bottom sheet presentation;
- `app-player-settings-panel` — the panel / bottom sheet presentation,
whose radio groups use `settings-radio-group.directive.ts`;
- `ControlsUpNext` and `app-player-up-next-card` — the "Up next" card's
gate and presentation; and
- `controls-view-model.ts` — derived display state.
@@ -346,7 +347,11 @@ app's `--app-selection-color` is a different blue that would fight the video.
measured below 3:1 against the lighter fills.
- **Icon buttons** are 40px with a 12px radius (32px / 9px compact) through
Material's `--mat-icon-button-*` tokens; their hover is a flat
`rgba(255,255,255,.1)` layer.
`rgba(255,255,255,.1)` layer. Keyboard focus draws a 2px `--pc-text`
outline (`:focus-visible`), and Material's focus state layer is switched
off: its colour comes from the app theme, and the light theme's dark layer
left no visible focus on video. The settings panel's icon buttons use the
same ring, inset by 2px because the panel body clips at its edge.
### Timeline segments
@@ -458,7 +463,10 @@ the panel the moment the last group disappears.
- **Roomy wide dock** (≥ 960px): two **value chips** precede `tune` — subtitles
(`closed_caption` + the selected track's label, or "Off") and speed
(`speed` + `1.25×`). Audio and aspect ratio have no chip: they are
(`speed` + `1.25×`). A chip's accessible name and tooltip carry its
value (`SUBTITLES_TOOLTIP` "Subtitles: English", `SPEED_TOOLTIP`
"Speed: 1.25×"); `tune` has `aria-haspopup="dialog"` and
`aria-expanded`. Audio and aspect ratio have no chip: they are
panel-only. A chip click opens the panel **focused on its group**
(`settingsFocus`; the group scrolls into view and wears a brief ring);
right-click or long-press on the subtitle chip toggles subtitles without
@@ -476,12 +484,34 @@ the panel the moment the last group disappears.
picture and recording stay in the compact dock — the mock shows only
`tune` + fullscreen there, but those two are engine features a viewer
needs without opening anything.
- **Inside**: list groups (audio, subtitles, quality) use `menuitemradio`
rows with a check mark and a cyan selection; segmented groups (speed,
aspect) use `radio` items with a violet selection, and a selected default
(`1×`, the first aspect preset) stays neutral. The subtitle group carries
the load-file action and the delay / size / color sections that the
popover used to hold (same `player-controls-load-subtitle`,
- **Inside**: every choice is a `radio` in a `radiogroup`
(`SettingsRadioGroupDirective` / `SettingsRadioDirective`). List groups
(audio, subtitles, quality) are rows with a check mark and a cyan
selection; segmented groups (speed, aspect, subtitle size) have a violet
selection, and a selected default (`1×`, the first aspect preset) stays
neutral. Each group is one Tab stop — the checked option as the engine
reports it, else the first — and arrows, Home and End move focus with a
CDK `FocusKeyManager` (wrapping; the horizontal arrows follow `direction`)
and check the option they reach, as a native radio group does: the
directive clicks it, so the template's handler applies the choice, even
onto an option the engine still reports as checked: returning to it must
cancel a switch that is still pending. Focus
changes also write the roving `tabindex` immediately, because a quick
Shift+Tab, Tab can arrive before change detection updates the bindings. The dialog is named by its `h2` title (`aria-labelledby`), each
radio group by its `h3` group heading or `h4` subheading, and the delay
buttons form a labelled `group`. Headings read `--pc-text-secondary` on
`--pc-glass-bg-dense` (`rgba(12,16,23,.86)`), 4.5:1 or more even over a
white frame. They wrap with `overflow-wrap: anywhere` and `hyphens: auto`,
so one long word (German "Wiedergabegeschwindigkeit") breaks inside the
sheet's 84px heading column. Hyphenation needs a `lang` on `<html>`.
With subtitles on but no track marked selected yet (the engine can report
the switch before the track list), the subtitle chip reads "On"
(`SUBTITLES_ON`), never "Off".
Every focused option shows the `--pc-text` ring; a selected swatch has a
white border with a dark inner gap, and focus adds an outer ring. The
subtitle group carries the load-file action (a plain button outside the
radio group) and the delay / size / color sections that the popover used
to hold (same `player-controls-load-subtitle`,
`player-controls-subtitle-delay`, `player-controls-subtitle-style` test
ids). The panel is a `role="dialog"` with `tabindex="-1"`: opened from
the keyboard (the opener is `:focus-visible`) it takes focus, a pointer
@@ -1639,6 +1669,7 @@ libs/ui/playback/src/lib/player-controls/
├── player-settings-panel.component.ts
├── player-settings-panel.component.html
├── player-settings-panel.component.scss
├── settings-radio-group.directive.ts
├── controls-fullscreen.ts
├── controls-menu-selection.ts
├── controls-menu-state.ts