mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
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>
This commit is contained in:
1 parent
cdc208dbff
commit
6c050f3175
25 files changed
+140
-66
No files matched your search
@@ -247,38 +247,38 @@ test('@web @playback keyboard reaches the dock and the settings radios with a vi
|
||||
]);
|
||||
expect(new Set(radioStops.map((stop) => stop.group)).size).toBe(3);
|
||||
|
||||
// --- Arrows move focus without applying; Space applies. -----------
|
||||
// --- Arrows move focus and apply the option they reach. ----------
|
||||
const speedGroup = panel.locator(
|
||||
'[data-test-id="player-settings-speed"] [role="radiogroup"]'
|
||||
);
|
||||
const rateBefore = await video.evaluate(
|
||||
(el: HTMLVideoElement) => el.playbackRate
|
||||
);
|
||||
const rate = () =>
|
||||
video.evaluate((el: HTMLVideoElement) => el.playbackRate);
|
||||
await page.keyboard.press('ArrowRight');
|
||||
const moved = await focusStop(page);
|
||||
expect(moved?.role).toBe('radio');
|
||||
expect(moved?.checked).toBe('false');
|
||||
expect(
|
||||
await video.evaluate((el: HTMLVideoElement) => el.playbackRate)
|
||||
).toBe(rateBefore);
|
||||
await page.keyboard.press('Space');
|
||||
await expect
|
||||
.poll(() =>
|
||||
video.evaluate((el: HTMLVideoElement) => el.playbackRate)
|
||||
)
|
||||
.toBe(Number.parseFloat(moved?.label ?? ''));
|
||||
await expect.poll(rate).toBe(Number.parseFloat(moved?.label ?? ''));
|
||||
await expect(
|
||||
speedGroup.locator('[role="radio"][aria-checked="true"]')
|
||||
).toHaveText(moved?.label ?? '');
|
||||
await page.keyboard.press('Home');
|
||||
expect((await focusStop(page))?.label).toBe('0.5×');
|
||||
await expect.poll(rate).toBe(0.5);
|
||||
await expect(
|
||||
speedGroup.locator('[role="radio"][aria-checked="true"]')
|
||||
).toHaveText('0.5×');
|
||||
// The ends wrap, as in a native radio group.
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
expect((await focusStop(page))?.label).toBe('2×');
|
||||
await expect.poll(rate).toBe(2);
|
||||
await expect(speedGroup.locator('[tabindex="0"]')).toHaveText('2×');
|
||||
// Leaving and re-entering lands on the checked option again.
|
||||
// Leaving and re-entering lands on the checked option — the one the
|
||||
// engine reports, so wait for it to confirm the switch first.
|
||||
await expect(
|
||||
speedGroup.locator('[role="radio"][aria-checked="true"]')
|
||||
).toHaveText('2×');
|
||||
await pressTab(page, browserName, 'backward');
|
||||
await pressTab(page, browserName);
|
||||
expect((await focusStop(page))?.label).toBe(moved?.label);
|
||||
expect((await focusStop(page))?.label).toBe('2×');
|
||||
|
||||
// --- A focused selected swatch differs from a selected one. -------
|
||||
await tabUntil(page, browserName, {
|
||||
@@ -286,14 +286,24 @@ test('@web @playback keyboard reaches the dock and the settings radios with a vi
|
||||
direction: 'backward',
|
||||
max: 5,
|
||||
});
|
||||
const focusedSelected = panel.locator('.player-settings__swatch:focus');
|
||||
await expect(focusedSelected).toHaveAttribute('aria-checked', 'true');
|
||||
await expect(focusedSelected).toHaveCSS('outline-style', 'solid');
|
||||
const focusedSwatch = panel.locator('.player-settings__swatch:focus');
|
||||
await expect(focusedSwatch).toHaveAttribute('aria-checked', 'true');
|
||||
await expect(focusedSwatch).toHaveCSS('outline-style', 'solid');
|
||||
// The arrow checks the next swatch, which takes focus and the ring.
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(focusedSwatch).toHaveAttribute('aria-checked', 'true');
|
||||
await expect(focusedSwatch).toHaveCSS('outline-style', 'solid');
|
||||
// Tab moves on to the speed group: the checked swatch keeps its
|
||||
// selected border but loses the ring.
|
||||
await pressTab(page, browserName);
|
||||
expect(
|
||||
await focusIsInside(page, '[data-test-id="player-settings-speed"]')
|
||||
).toBe(true);
|
||||
const selectedOnly = panel.locator(
|
||||
'.player-settings__swatch[aria-checked="true"]:not(:focus)'
|
||||
'.player-settings__swatch[aria-checked="true"]'
|
||||
);
|
||||
await expect(selectedOnly).toHaveCount(1);
|
||||
await expect(selectedOnly).not.toBeFocused();
|
||||
await expect(selectedOnly).toHaveCSS('outline-style', 'none');
|
||||
await expect(selectedOnly).toHaveCSS(
|
||||
'border-top-color',
|
||||
@@ -313,13 +323,9 @@ test('@web @playback keyboard reaches the dock and the settings radios with a vi
|
||||
path: test.info().outputPath(`${theme}-panel-focus.png`),
|
||||
});
|
||||
|
||||
// A keyboard-focused swatch shows its tooltip, and Material spends
|
||||
// the next Escape on a visible tooltip: close from a speed radio,
|
||||
// which has none, once the swatch's tooltip has gone.
|
||||
await pressTab(page, browserName);
|
||||
expect(
|
||||
await focusIsInside(page, '[data-test-id="player-settings-speed"]')
|
||||
).toBe(true);
|
||||
// Focus is on a speed radio, which has no tooltip. Material spends
|
||||
// an Escape on any tooltip still showing (the swatch's fades out),
|
||||
// so wait for it to go before closing.
|
||||
await expect(page.locator('.mat-mdc-tooltip')).toHaveCount(0);
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(panel).toHaveCount(0);
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "مسارات الصوت",
|
||||
"SUBTITLES": "الترجمات",
|
||||
"SUBTITLES_OFF": "إيقاف",
|
||||
"SUBTITLES_ON": "تشغيل",
|
||||
"QUALITY": "الجودة",
|
||||
"QUALITY_AUTO": "تلقائي",
|
||||
"TRACK_DEFAULT": "افتراضي",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "مسارات الصوت",
|
||||
"SUBTITLES": "الترجمة",
|
||||
"SUBTITLES_OFF": "معطلة",
|
||||
"SUBTITLES_ON": "مفعلة",
|
||||
"QUALITY": "الجودة",
|
||||
"QUALITY_AUTO": "تلقائي",
|
||||
"TRACK_DEFAULT": "افتراضي",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Аўдыядарожкі",
|
||||
"SUBTITLES": "Субтытры",
|
||||
"SUBTITLES_OFF": "Выкл.",
|
||||
"SUBTITLES_ON": "Укл.",
|
||||
"QUALITY": "Якасць",
|
||||
"QUALITY_AUTO": "Аўта",
|
||||
"TRACK_DEFAULT": "Па змаўчанні",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Tonspuren",
|
||||
"SUBTITLES": "Untertitel",
|
||||
"SUBTITLES_OFF": "Aus",
|
||||
"SUBTITLES_ON": "An",
|
||||
"QUALITY": "Qualität",
|
||||
"QUALITY_AUTO": "Automatisch",
|
||||
"TRACK_DEFAULT": "Standard",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Κομμάτια ήχου",
|
||||
"SUBTITLES": "Υπότιτλοι",
|
||||
"SUBTITLES_OFF": "Ανενεργοί",
|
||||
"SUBTITLES_ON": "Ενεργοί",
|
||||
"QUALITY": "Ποιότητα",
|
||||
"QUALITY_AUTO": "Αυτόματη",
|
||||
"TRACK_DEFAULT": "Προεπιλογή",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Audio tracks",
|
||||
"SUBTITLES": "Subtitles",
|
||||
"SUBTITLES_OFF": "Off",
|
||||
"SUBTITLES_ON": "On",
|
||||
"QUALITY": "Quality",
|
||||
"QUALITY_AUTO": "Auto",
|
||||
"TRACK_DEFAULT": "Default",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Pistas de audio",
|
||||
"SUBTITLES": "Subtítulos",
|
||||
"SUBTITLES_OFF": "Desactivados",
|
||||
"SUBTITLES_ON": "Activados",
|
||||
"QUALITY": "Calidad",
|
||||
"QUALITY_AUTO": "Automática",
|
||||
"TRACK_DEFAULT": "Predeterminada",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Pistes audio",
|
||||
"SUBTITLES": "Sous-titres",
|
||||
"SUBTITLES_OFF": "Désactivés",
|
||||
"SUBTITLES_ON": "Activés",
|
||||
"QUALITY": "Qualité",
|
||||
"QUALITY_AUTO": "Auto",
|
||||
"TRACK_DEFAULT": "Par défaut",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Hangsávok",
|
||||
"SUBTITLES": "Feliratok",
|
||||
"SUBTITLES_OFF": "Kikapcsolva",
|
||||
"SUBTITLES_ON": "Bekapcsolva",
|
||||
"QUALITY": "Minőség",
|
||||
"QUALITY_AUTO": "Automatikus",
|
||||
"TRACK_DEFAULT": "Alapértelmezett",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Tracce audio",
|
||||
"SUBTITLES": "Sottotitoli",
|
||||
"SUBTITLES_OFF": "Disattivati",
|
||||
"SUBTITLES_ON": "Attivati",
|
||||
"QUALITY": "Qualità",
|
||||
"QUALITY_AUTO": "Automatica",
|
||||
"TRACK_DEFAULT": "Predefinita",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "音声トラック",
|
||||
"SUBTITLES": "字幕",
|
||||
"SUBTITLES_OFF": "オフ",
|
||||
"SUBTITLES_ON": "オン",
|
||||
"QUALITY": "画質",
|
||||
"QUALITY_AUTO": "自動",
|
||||
"TRACK_DEFAULT": "デフォルト",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "오디오 트랙",
|
||||
"SUBTITLES": "자막",
|
||||
"SUBTITLES_OFF": "끄기",
|
||||
"SUBTITLES_ON": "켜기",
|
||||
"QUALITY": "화질",
|
||||
"QUALITY_AUTO": "자동",
|
||||
"TRACK_DEFAULT": "기본",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Audiosporen",
|
||||
"SUBTITLES": "Ondertiteling",
|
||||
"SUBTITLES_OFF": "Uit",
|
||||
"SUBTITLES_ON": "Aan",
|
||||
"QUALITY": "Kwaliteit",
|
||||
"QUALITY_AUTO": "Automatisch",
|
||||
"TRACK_DEFAULT": "Standaard",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Ścieżki audio",
|
||||
"SUBTITLES": "Napisy",
|
||||
"SUBTITLES_OFF": "Wyłączone",
|
||||
"SUBTITLES_ON": "Włączone",
|
||||
"QUALITY": "Jakość",
|
||||
"QUALITY_AUTO": "Automatycznie",
|
||||
"TRACK_DEFAULT": "Domyślna",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Faixas de áudio",
|
||||
"SUBTITLES": "Legendas",
|
||||
"SUBTITLES_OFF": "Desativadas",
|
||||
"SUBTITLES_ON": "Ativadas",
|
||||
"QUALITY": "Qualidade",
|
||||
"QUALITY_AUTO": "Automática",
|
||||
"TRACK_DEFAULT": "Padrão",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Аудиодорожки",
|
||||
"SUBTITLES": "Субтитры",
|
||||
"SUBTITLES_OFF": "Выкл.",
|
||||
"SUBTITLES_ON": "Вкл.",
|
||||
"QUALITY": "Качество",
|
||||
"QUALITY_AUTO": "Авто",
|
||||
"TRACK_DEFAULT": "По умолчанию",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "Ses parçaları",
|
||||
"SUBTITLES": "Altyazılar",
|
||||
"SUBTITLES_OFF": "Kapalı",
|
||||
"SUBTITLES_ON": "Açık",
|
||||
"QUALITY": "Kalite",
|
||||
"QUALITY_AUTO": "Otomatik",
|
||||
"TRACK_DEFAULT": "Varsayılan",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "音轨",
|
||||
"SUBTITLES": "字幕",
|
||||
"SUBTITLES_OFF": "关闭",
|
||||
"SUBTITLES_ON": "开启",
|
||||
"QUALITY": "画质",
|
||||
"QUALITY_AUTO": "自动",
|
||||
"TRACK_DEFAULT": "默认",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"AUDIO_TRACKS": "音軌",
|
||||
"SUBTITLES": "字幕",
|
||||
"SUBTITLES_OFF": "關閉",
|
||||
"SUBTITLES_ON": "開啟",
|
||||
"QUALITY": "畫質",
|
||||
"QUALITY_AUTO": "自動",
|
||||
"TRACK_DEFAULT": "預設",
|
||||
|
||||
@@ -473,20 +473,23 @@ the panel the moment the last group disappears.
|
||||
(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, else the first —
|
||||
and arrows, Home and End move focus with a CDK `FocusKeyManager`
|
||||
(wrapping; the horizontal arrows follow `direction`). Moving focus does not
|
||||
apply: a choice is applied to the playing stream at once and a track or
|
||||
quality switch rebuffers, so Space / Enter checks, as in a toolbar radio
|
||||
group. 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
|
||||
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. An
|
||||
option the engine already reports as checked is not applied again. 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
|
||||
|
||||
@@ -366,11 +366,16 @@
|
||||
settings.available() && layout.roomy() && !settings.isOpen()
|
||||
) {
|
||||
@if (settings.groups().subtitles) {
|
||||
<!-- On with no track marked selected yet (the engine
|
||||
can report the id before the list) reads "On",
|
||||
never "Off". The name carries the value
|
||||
("Subtitles: English"); the label is the value. -->
|
||||
@let subtitleValue =
|
||||
settings.subtitleLabel() ??
|
||||
('EMBEDDED_MPV.PLAYER.SUBTITLES_OFF' | translate);
|
||||
<!-- The name carries the value ("Subtitles: English"):
|
||||
the label alone is only the value. -->
|
||||
((settings.subtitlesOn()
|
||||
? 'EMBEDDED_MPV.PLAYER.SUBTITLES_ON'
|
||||
: 'EMBEDDED_MPV.PLAYER.SUBTITLES_OFF'
|
||||
) | translate);
|
||||
<button
|
||||
type="button"
|
||||
class="player-controls__chip"
|
||||
|
||||
@@ -127,6 +127,7 @@ describe('PlayerSettingsPanelComponent accessibility', () => {
|
||||
AUDIO_TRACKS: 'Audio tracks',
|
||||
SUBTITLES: 'Subtitles',
|
||||
SUBTITLES_OFF: 'Off',
|
||||
SUBTITLES_ON: 'On',
|
||||
SUBTITLES_TOOLTIP: 'Subtitles: {{subtitles}}',
|
||||
SUBTITLE_DELAY: 'Subtitle delay',
|
||||
SUBTITLE_SIZE: 'Subtitle size',
|
||||
@@ -221,10 +222,9 @@ describe('PlayerSettingsPanelComponent accessibility', () => {
|
||||
expect(tabStops('aspect')).toEqual(['Default']);
|
||||
});
|
||||
|
||||
it('moves focus with the arrow keys, Home and End without applying a choice', () => {
|
||||
it('moves focus with the arrow keys, Home and End and checks the option reached', () => {
|
||||
openPanel();
|
||||
const speed = radios('speed');
|
||||
const checked = speed.find(
|
||||
const checked = radios('speed').find(
|
||||
(radio) => radio.getAttribute('aria-checked') === 'true'
|
||||
) as HTMLButtonElement;
|
||||
checked.focus();
|
||||
@@ -235,24 +235,33 @@ describe('PlayerSettingsPanelComponent accessibility', () => {
|
||||
expect(document.activeElement?.textContent?.trim()).toBe('1.25×');
|
||||
expect(tabStops('speed')).toEqual(['1.25×']);
|
||||
|
||||
press(document.activeElement as HTMLElement, 'ArrowDown');
|
||||
expect(document.activeElement?.textContent?.trim()).toBe('1.5×');
|
||||
press(document.activeElement as HTMLElement, 'ArrowUp');
|
||||
press(document.activeElement as HTMLElement, 'ArrowLeft');
|
||||
expect(document.activeElement?.textContent?.trim()).toBe('1×');
|
||||
press(document.activeElement as HTMLElement, 'End');
|
||||
expect(document.activeElement?.textContent?.trim()).toBe('2×');
|
||||
// The ends wrap, as in a native radio group.
|
||||
press(document.activeElement as HTMLElement, 'ArrowRight');
|
||||
expect(document.activeElement?.textContent?.trim()).toBe('0.5×');
|
||||
press(document.activeElement as HTMLElement, 'ArrowLeft');
|
||||
expect(document.activeElement?.textContent?.trim()).toBe('2×');
|
||||
press(document.activeElement as HTMLElement, 'Home');
|
||||
expect(document.activeElement?.textContent?.trim()).toBe('0.5×');
|
||||
|
||||
expect(fake.commands.setPlaybackSpeed).not.toHaveBeenCalled();
|
||||
(document.activeElement as HTMLButtonElement).click();
|
||||
expect(fake.commands.setPlaybackSpeed).toHaveBeenCalledWith(0.5);
|
||||
const reached = [
|
||||
'ArrowDown',
|
||||
'ArrowUp',
|
||||
'ArrowLeft',
|
||||
'End',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'Home',
|
||||
].map((key) => {
|
||||
press(document.activeElement as HTMLElement, key);
|
||||
return document.activeElement?.textContent?.trim();
|
||||
});
|
||||
expect(reached).toEqual([
|
||||
'1.5×',
|
||||
'1.25×',
|
||||
'1×',
|
||||
'2×',
|
||||
'0.5×',
|
||||
'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.
|
||||
expect(
|
||||
fake.commands.setPlaybackSpeed.mock.calls.map(([speed]) => speed)
|
||||
).toEqual([1.25, 1.5, 1.25, 2, 0.5, 2, 0.5]);
|
||||
});
|
||||
|
||||
it('keeps arrow keys inside their own group', () => {
|
||||
@@ -263,10 +272,13 @@ describe('PlayerSettingsPanelComponent accessibility', () => {
|
||||
expect(document.activeElement).toBe(german);
|
||||
press(german, 'ArrowDown');
|
||||
expect(document.activeElement).toBe(english);
|
||||
expect(fake.commands.setAudioTrack).not.toHaveBeenCalled();
|
||||
expect(fake.commands.setAudioTrack.mock.calls).toEqual([[2]]);
|
||||
expect(fake.commands.setPlaybackSpeed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hands the Tab stop back to the checked option when focus leaves', () => {
|
||||
it('hands the Tab stop back to the reported checked option when focus leaves', () => {
|
||||
// The fake engine never confirms the switch the arrow key requested,
|
||||
// so English stays the checked track.
|
||||
openPanel();
|
||||
const [english, german] = radios('audio');
|
||||
english.focus();
|
||||
@@ -316,6 +328,23 @@ describe('PlayerSettingsPanelComponent accessibility', () => {
|
||||
expect(speedChip()?.getAttribute('aria-label')).toBe('Speed: 1.25×');
|
||||
});
|
||||
|
||||
it('names the subtitle chip "On" while no enabled track is marked selected', () => {
|
||||
// The engine can report subtitles on before the track list marks the
|
||||
// selected track; the chip must not claim they are off.
|
||||
fake.state.update((state) => ({
|
||||
...state,
|
||||
subtitleTracks: [{ id: 5, label: 'Russian', selected: false }],
|
||||
subtitlesEnabled: true,
|
||||
}));
|
||||
fixture.detectChanges();
|
||||
|
||||
const chip = query('[data-test-id="player-controls-subtitle-chip"]');
|
||||
expect(chip?.getAttribute('aria-label')).toBe('Subtitles: On');
|
||||
expect(
|
||||
chip?.querySelector('.player-controls__chip-label')?.textContent
|
||||
).toContain('On');
|
||||
});
|
||||
|
||||
it('announces that the tune button opens a dialog', () => {
|
||||
const tune = query('[data-test-id="player-controls-settings-button"]');
|
||||
expect(tune?.getAttribute('aria-haspopup')).toBe('dialog');
|
||||
|
||||
@@ -349,7 +349,9 @@
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 2px 4px 4px;
|
||||
// Room on every side for the focus ring (2px offset + 2px): the body
|
||||
// scrolls and would clip it at its edge.
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.player-settings__swatch {
|
||||
|
||||
@@ -42,6 +42,13 @@ export class SettingsRadioDirective implements FocusableOption {
|
||||
this.group.onOptionFocus(this);
|
||||
}
|
||||
|
||||
/** Checks the option through its own click handler, as a press would. */
|
||||
check(): void {
|
||||
if (!this.checked()) {
|
||||
this.element.click();
|
||||
}
|
||||
}
|
||||
|
||||
/** Immediate write of what the `tabindex` binding renders next. */
|
||||
setTabStop(isTabStop: boolean): void {
|
||||
this.element.tabIndex = isTabStop ? 0 : -1;
|
||||
@@ -52,11 +59,9 @@ export class SettingsRadioDirective implements FocusableOption {
|
||||
* A radio group in the settings panel (audio, subtitles, quality, speed,
|
||||
* aspect, subtitle size and colour). One option is a Tab stop — the checked
|
||||
* one, or the first when none is — and the arrow keys, Home and End move
|
||||
* focus between the options with a CDK `FocusKeyManager`. Moving focus does
|
||||
* not check: every choice is applied to the playing stream at once, and a
|
||||
* track or quality switch rebuffers, so arrowing through five levels must
|
||||
* not switch five times. Space or Enter checks the focused option, as in a
|
||||
* toolbar radio group.
|
||||
* focus between the options with a CDK `FocusKeyManager` and check the
|
||||
* option they reach, as a native radio group does. Checking clicks the
|
||||
* option, so the template's handler applies the choice to the player.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[appSettingsRadioGroup]',
|
||||
@@ -104,7 +109,12 @@ export class SettingsRadioGroupDirective {
|
||||
onKeydown(event: KeyboardEvent): void {
|
||||
const rtl = getComputedStyle(this.host).direction === 'rtl';
|
||||
this.keyManager.withHorizontalOrientation(rtl ? 'rtl' : 'ltr');
|
||||
const previous = this.keyManager.activeItem;
|
||||
this.keyManager.onKeydown(event);
|
||||
const next = this.keyManager.activeItem;
|
||||
if (next && next !== previous) {
|
||||
next.check();
|
||||
}
|
||||
}
|
||||
|
||||
/** Leaving the group hands the Tab stop back to the checked option. */
|
||||
|
||||
Reference in new issue
Block a user