diff --git a/docs/architecture/player-controls-contract.md b/docs/architecture/player-controls-contract.md index 07fae5b23..4f6049263 100644 --- a/docs/architecture/player-controls-contract.md +++ b/docs/architecture/player-controls-contract.md @@ -477,8 +477,9 @@ the panel the moment the last group disappears. 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. An - option the engine already reports as checked is not applied again. Focus + 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 diff --git a/libs/ui/playback/src/lib/player-controls/player-settings-panel.a11y.spec.ts b/libs/ui/playback/src/lib/player-controls/player-settings-panel.a11y.spec.ts index 8f5770238..1e2e25b98 100644 --- a/libs/ui/playback/src/lib/player-controls/player-settings-panel.a11y.spec.ts +++ b/libs/ui/playback/src/lib/player-controls/player-settings-panel.a11y.spec.ts @@ -257,11 +257,11 @@ describe('PlayerSettingsPanelComponent accessibility', () => { '2×', '0.5×', ]); - // Every move applies its option — except onto 1×, which the (fake) - // engine still reports as checked, so it is not applied again. + // Every move applies its option, 1× included: the (fake) engine + // still reports 1× checked, but 1.25× is the pending request. expect( fake.commands.setPlaybackSpeed.mock.calls.map(([speed]) => speed) - ).toEqual([1.25, 1.5, 1.25, 2, 0.5, 2, 0.5]); + ).toEqual([1.25, 1.5, 1.25, 1, 2, 0.5, 2, 0.5]); }); it('keeps arrow keys inside their own group', () => { @@ -272,7 +272,9 @@ describe('PlayerSettingsPanelComponent accessibility', () => { expect(document.activeElement).toBe(german); press(german, 'ArrowDown'); expect(document.activeElement).toBe(english); - expect(fake.commands.setAudioTrack.mock.calls).toEqual([[2]]); + // Returning before the engine confirms the first switch applies + // English again; otherwise the pending switch would win. + expect(fake.commands.setAudioTrack.mock.calls).toEqual([[2], [1]]); expect(fake.commands.setPlaybackSpeed).not.toHaveBeenCalled(); }); diff --git a/libs/ui/playback/src/lib/player-controls/settings-radio-group.directive.ts b/libs/ui/playback/src/lib/player-controls/settings-radio-group.directive.ts index fb75afe29..150010958 100644 --- a/libs/ui/playback/src/lib/player-controls/settings-radio-group.directive.ts +++ b/libs/ui/playback/src/lib/player-controls/settings-radio-group.directive.ts @@ -42,11 +42,14 @@ export class SettingsRadioDirective implements FocusableOption { this.group.onOptionFocus(this); } - /** Checks the option through its own click handler, as a press would. */ + /** + * Checks the option through its own click handler, as a press would. + * Unconditionally: the engine may still report this option checked + * while a switch away from it is pending, and returning to it must + * cancel that switch. + */ check(): void { - if (!this.checked()) { - this.element.click(); - } + this.element.click(); } /** Immediate write of what the `tabindex` binding renders next. */