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>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-30 22:15:56 +02:00
1 parent 6c050f3175
commit 6c6bf6da4b
3 files changed
+16 -10

No files matched your search

@@ -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
@@ -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();
});
@@ -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. */