From f1c0468f70333678bfb7ee73f74846b365bbf796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=28=EA=B9=80=EA=B7=9C=ED=9A=8C=29?= <48755156+KimKyuHoi@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:53:23 +0900 Subject: [PATCH] feat(UI): Improve shaka player UI accessibility (#10023) ### ARIA attributes - Add `aria-hidden="true"` to all decorative SVG icons (`icon.js`) - Implement missing `aria-label` for ad controls (`skip_ad_button.js`, `ad_info.js`) - Add `aria-pressed` to toggle buttons (mute, fullscreen, pip, loop, statistics, remote, stereoscopic) - Remove incorrect `aria-pressed` from one-shot action button (`recenter_vr.js`) - Add `role="menu"`, `aria-haspopup`, `aria-expanded` to menus (`overflow_menu.js`, `settings_menu.js`, `context_menu.js`) - Add `role="menuitemradio"` and `aria-checked` to selection menus (resolution, language, text, playback rate, etc.) - Add `role="toolbar"` to control panel (`controls.js`) - Add `role="heading"` to content title (`content_title.js`) ### Focus management - Restore focus to trigger button when menus close - Maintain focus on fullscreen button after toggle ### CSS - Add `forced-colors` media query for Windows high contrast mode ### Other - Add `alt=""` to seek bar thumbnail image - Add `aria-hidden="true"` to watermark canvas - Add accessibility unit tests for ARIA attributes - Remove redundant `aria-hidden` from checkmark icon (`ui_utils.js`) - Add ARIA terms to project spell-check dictionary Issue https://github.com/shaka-project/shaka-player/issues/3146 --- project-words.txt | 2 + test/ui/ui_unit.js | 76 ++++++++++++++++++++++++++++++++++- ui/ad_info.js | 4 +- ui/ad_statistics_button.js | 5 +++ ui/chapter_selection.js | 1 + ui/content_title.js | 2 + ui/context_menu.js | 1 + ui/controls.js | 2 + ui/fullscreen_button.js | 3 ++ ui/icon.js | 3 ++ ui/language_utils.js | 10 ++++- ui/less/general.less | 8 ++++ ui/loop_button.js | 2 + ui/mute_button.js | 4 ++ ui/overflow_menu.js | 6 +++ ui/pip_button.js | 3 ++ ui/playback_rate_selection.js | 5 ++- ui/recenter_vr.js | 1 - ui/remote_button.js | 2 + ui/resolution_selection.js | 15 +++++-- ui/seek_bar.js | 1 + ui/settings_menu.js | 8 ++++ ui/skip_ad_button.js | 3 +- ui/statistics_button.js | 5 +++ ui/text_position.js | 7 +++- ui/text_selection.js | 8 +++- ui/text_size.js | 7 +++- ui/toggle_stereoscopic.js | 2 + ui/ui_utils.js | 3 +- ui/video_type_selection.js | 5 ++- ui/watermark.js | 1 + 31 files changed, 185 insertions(+), 20 deletions(-) diff --git a/project-words.txt b/project-words.txt index 0ab3191c6..be2c882b3 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1,5 +1,7 @@ # events / html abrstatuschanged +haspopup +menuitemradio adblocker audiofocuspaused audiofocusgranted diff --git a/test/ui/ui_unit.js b/test/ui/ui_unit.js index adefa3ff3..63f50a860 100644 --- a/test/ui/ui_unit.js +++ b/test/ui/ui_unit.js @@ -385,6 +385,17 @@ describe('UI', () => { } } }); + + it('has correct ARIA roles', () => { + expect(overflowMenu.getAttribute('role')).toBe('menu'); + }); + + it('has aria-haspopup and aria-expanded on menu button', () => { + const menuButton = videoContainer.getElementsByClassName( + 'shaka-overflow-menu-button')[0]; + expect(menuButton.getAttribute('aria-haspopup')).toBe('true'); + expect(menuButton.getAttribute('aria-expanded')).toBe('false'); + }); }); describe('controls-button-panel', () => { @@ -502,6 +513,39 @@ describe('UI', () => { confirmAriaLabel('shaka-fast-forward-button'); confirmAriaLabel('shaka-rewind-button'); }); + + it('has toolbar role', async () => { + await UiUtils.createUIThroughAPI(videoContainer, video); + const controlsButtonPanels = videoContainer.getElementsByClassName( + 'shaka-controls-button-panel'); + expect(controlsButtonPanels.length).toBe(1); + expect(controlsButtonPanels[0].getAttribute('role')).toBe('toolbar'); + }); + + it('has aria-pressed on toggle buttons', async () => { + const config = { + controlPanelElements: [ + 'mute', + 'fullscreen', + ], + }; + + await UiUtils.createUIThroughAPI(videoContainer, video, config); + const muteButton = videoContainer.getElementsByClassName( + 'shaka-mute-button')[0]; + const fullscreenButton = videoContainer.getElementsByClassName( + 'shaka-fullscreen-button')[0]; + expect(muteButton.hasAttribute('aria-pressed')).toBe(true); + expect(fullscreenButton.hasAttribute('aria-pressed')).toBe(true); + }); + + it('has aria-hidden on SVG icons', async () => { + await UiUtils.createUIThroughAPI(videoContainer, video); + const icons = videoContainer.getElementsByClassName('shaka-ui-icon'); + for (const icon of icons) { + expect(icon.getAttribute('aria-hidden')).toBe('true'); + } + }); }); describe('control panel buttons with submenus', () => { @@ -583,6 +627,33 @@ describe('UI', () => { expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(true); expect(languageMenu.classList.contains('shaka-hidden')).toBe(false); }); + + it('settings menus have ARIA roles', () => { + expect(resolutionMenu.getAttribute('role')).toBe('menu'); + expect(languageMenu.getAttribute('role')).toBe('menu'); + }); + + it('settings menu buttons have aria-haspopup and aria-expanded', () => { + expect(resolutionMenuButton.getAttribute('aria-haspopup')) + .toBe('true'); + expect(resolutionMenuButton.getAttribute('aria-expanded')) + .toBe('false'); + + expect(languageMenuButton.getAttribute('aria-haspopup')) + .toBe('true'); + expect(languageMenuButton.getAttribute('aria-expanded')) + .toBe('false'); + }); + + it('aria-expanded updates when menu opens and closes', () => { + resolutionMenuButton.click(); + expect(resolutionMenuButton.getAttribute('aria-expanded')) + .toBe('true'); + + resolutionMenuButton.click(); + expect(resolutionMenuButton.getAttribute('aria-expanded')) + .toBe('false'); + }); }); describe('resolutions menu', () => { @@ -854,8 +925,9 @@ describe('UI', () => { it('builds internal elements', () => { expect(contextMenu.childNodes.length).toBe(1); - expect(contextMenu.childNodes[0]['className']) - .toBe('shaka-statistics-button'); + const element = /** @type {!HTMLElement} */ (contextMenu.childNodes[0]); + expect(element.classList.contains('shaka-statistics-button')) + .toBe(true); }); }); diff --git a/ui/ad_info.js b/ui/ad_info.js index 23e8b7298..a1c97e50c 100644 --- a/ui/ad_info.js +++ b/ui/ad_info.js @@ -77,7 +77,7 @@ shaka.ui.AdInfo = class extends shaka.ui.Element { * @private */ updateAriaLabel_() { - // TODO + // arai-label is set dynamically in onTimerTick_(). } /** @@ -119,6 +119,7 @@ shaka.ui.AdInfo = class extends shaka.ui.Element { if (secondsLeft == -1 || adDuration == -1 || !isFinite(secondsLeft) || !isFinite(adDuration)) { this.adInfo_.textContent = text; + this.adInfo_.ariaLabel = text; shaka.ui.Utils.setDisplay(this.adInfo_, text != ''); return; } @@ -141,6 +142,7 @@ shaka.ui.AdInfo = class extends shaka.ui.Element { .replace('[AD_TIME]', timeString); } this.adInfo_.textContent = text; + this.adInfo_.ariaLabel = text; shaka.ui.Utils.setDisplay(this.adInfo_, text != ''); } else { this.reset_(); diff --git a/ui/ad_statistics_button.js b/ui/ad_statistics_button.js index 7b1ec6495..1dc589599 100644 --- a/ui/ad_statistics_button.js +++ b/ui/ad_statistics_button.js @@ -38,6 +38,9 @@ shaka.ui.AdStatisticsButton = class extends shaka.ui.Element { /** @private {!HTMLButtonElement} */ this.button_ = shaka.util.Dom.createButton(); this.button_.classList.add('shaka-ad-statistics-button'); + this.button_.classList.add('shaka-tooltip'); + this.button_.classList.add('shaka-no-propagation'); + this.button_.ariaPressed = 'false'; /** @private {!shaka.ui.Icon} */ this.icon_ = new shaka.ui.Icon(this.button_, @@ -156,10 +159,12 @@ shaka.ui.AdStatisticsButton = class extends shaka.ui.Element { this.icon_.use(shaka.ui.Enums.MaterialDesignSVGIcons['STATISTICS_OFF']); this.timer_.tickEvery(0.1); shaka.ui.Utils.setDisplay(this.container_, true); + this.button_.ariaPressed = 'true'; } else { this.icon_.use(shaka.ui.Enums.MaterialDesignSVGIcons['STATISTICS_ON']); this.timer_.stop(); shaka.ui.Utils.setDisplay(this.container_, false); + this.button_.ariaPressed = 'false'; } } diff --git a/ui/chapter_selection.js b/ui/chapter_selection.js index 08c14d66d..2788b16a9 100644 --- a/ui/chapter_selection.js +++ b/ui/chapter_selection.js @@ -114,6 +114,7 @@ shaka.ui.ChapterSelection = class extends shaka.ui.SettingsMenu { if (chapters.length) { for (const chapter of chapters) { const button = shaka.util.Dom.createButton(); + button.setAttribute('role', 'menuitem'); button.classList.add('shaka-chapter-item'); const span = shaka.util.Dom.createHTMLElement('span'); span.classList.add('shaka-chapter'); diff --git a/ui/content_title.js b/ui/content_title.js index bc3146880..f776f6f8c 100644 --- a/ui/content_title.js +++ b/ui/content_title.js @@ -34,6 +34,8 @@ shaka.ui.ContentTitle = class extends shaka.ui.Element { /** @type {!HTMLElement} */ this.title_ = shaka.util.Dom.createHTMLElement('div'); this.title_.classList.add('shaka-content-title'); + this.title_.setAttribute('role', 'heading'); + this.title_.setAttribute('aria-level', '2'); this.parent.appendChild(this.title_); this.eventManager.listen(this.player, 'unloading', () => { diff --git a/ui/context_menu.js b/ui/context_menu.js index ef3af5c19..095815ac7 100644 --- a/ui/context_menu.js +++ b/ui/context_menu.js @@ -44,6 +44,7 @@ shaka.ui.ContextMenu = class extends shaka.ui.Element { this.contextMenu_.classList.add('shaka-no-propagation'); this.contextMenu_.classList.add('shaka-context-menu'); this.contextMenu_.classList.add('shaka-hidden'); + this.contextMenu_.setAttribute('role', 'menu'); this.controlsContainer_.appendChild(this.contextMenu_); diff --git a/ui/controls.js b/ui/controls.js index e35e2b52d..2584861c0 100644 --- a/ui/controls.js +++ b/ui/controls.js @@ -1528,6 +1528,8 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { this.controlsButtonPanel_.classList.add('shaka-controls-button-panel'); this.controlsButtonPanel_.classList.add( 'shaka-show-controls-on-mouse-over'); + this.controlsButtonPanel_.setAttribute('role', 'toolbar'); + if (this.config_.enableTooltips) { this.controlsButtonPanel_.classList.add('shaka-tooltips-on'); } diff --git a/ui/fullscreen_button.js b/ui/fullscreen_button.js index 25dad198e..353f962eb 100644 --- a/ui/fullscreen_button.js +++ b/ui/fullscreen_button.js @@ -63,6 +63,7 @@ shaka.ui.FullscreenButton = class extends shaka.ui.Element { return; } await this.controls.toggleFullScreen(); + this.button_.focus(); }); this.eventManager.listen(document, 'fullscreenchange', () => { @@ -110,6 +111,8 @@ shaka.ui.FullscreenButton = class extends shaka.ui.Element { LocIds.EXIT_FULL_SCREEN : LocIds.FULL_SCREEN; this.button_.ariaLabel = this.localization.resolve(label); + this.button_.ariaPressed = + this.controls.isFullScreenEnabled() ? 'true' : 'false'; } /** diff --git a/ui/icon.js b/ui/icon.js index e2525db6f..a234d060a 100644 --- a/ui/icon.js +++ b/ui/icon.js @@ -24,6 +24,9 @@ shaka.ui.Icon = class { this.svg_ = shaka.util.Dom.createSVGElement('svg'); this.svg_.classList.add('shaka-ui-icon'); + // Screen reader should ignore icon text. + // all icons should have this attribute + this.svg_.ariaHidden = 'true'; this.svg_.setAttribute('viewBox', '0 -960 960 960'); if (icon) { diff --git a/ui/language_utils.js b/ui/language_utils.js index 58ab6a0ca..1013e7914 100644 --- a/ui/language_utils.js +++ b/ui/language_utils.js @@ -181,6 +181,9 @@ shaka.ui.LanguageUtils = class { button.addEventListener('click', () => { onTrackSelected(track); }); + // ARIA: single-select menu item + button.setAttribute('role', 'menuitemradio'); + button.setAttribute('aria-checked', 'false'); const span = shaka.util.Dom.createHTMLElement('span'); button.appendChild(span); @@ -262,7 +265,7 @@ shaka.ui.LanguageUtils = class { if (updateChosen && (combinationName == selectedCombination)) { button.appendChild(shaka.ui.Utils.checkmarkIcon()); span.classList.add('shaka-chosen-item'); - button.ariaSelected = 'true'; + button.setAttribute('aria-checked', 'true'); currentSelectionElement.textContent = span.textContent; } langMenu.appendChild(button); @@ -345,6 +348,9 @@ shaka.ui.LanguageUtils = class { button.addEventListener('click', () => { onTrackSelected(track); }); + // ARIA: single-select menu item + button.setAttribute('role', 'menuitemradio'); + button.setAttribute('aria-checked', 'false'); const span = shaka.util.Dom.createHTMLElement('span'); button.appendChild(span); @@ -433,7 +439,7 @@ shaka.ui.LanguageUtils = class { if (updateChosen && (combinationName == selectedCombination)) { button.appendChild(shaka.ui.Utils.checkmarkIcon()); span.classList.add('shaka-chosen-item'); - button.ariaSelected = 'true'; + button.setAttribute('aria-checked', 'true'); currentSelectionElement.textContent = span.textContent; } langMenu.appendChild(button); diff --git a/ui/less/general.less b/ui/less/general.less index 8a279d5ff..f960db4d5 100644 --- a/ui/less/general.less +++ b/ui/less/general.less @@ -145,3 +145,11 @@ the control buttons panel. */ @quality-mark-color: #fff; @quality-mark-hightlight-color: #f00; + +@media (forced-colors: active) { + .shaka-overflow-menu, + .shaka-settings-menu, + .shaka-context-menu { + border: 1px solid ButtonText; + } +} diff --git a/ui/loop_button.js b/ui/loop_button.js index 29becc487..f841611fe 100644 --- a/ui/loop_button.js +++ b/ui/loop_button.js @@ -39,6 +39,7 @@ shaka.ui.LoopButton = class extends shaka.ui.Element { this.button_.classList.add('shaka-loop-button'); this.button_.classList.add('shaka-tooltip'); this.button_.classList.add('shaka-no-propagation'); + this.button_.ariaPressed = 'false'; /** @private {!shaka.ui.Icon} */ this.icon_ = new shaka.ui.Icon(this.button_, @@ -181,6 +182,7 @@ shaka.ui.LoopButton = class extends shaka.ui.Element { LocIds.EXIT_LOOP_MODE : LocIds.ENTER_LOOP_MODE; this.button_.ariaLabel = this.localization.resolve(ariaText); + this.button_.ariaPressed = this.video.loop ? 'true' : 'false'; } diff --git a/ui/mute_button.js b/ui/mute_button.js index 53e2c375a..6c47fdcbd 100644 --- a/ui/mute_button.js +++ b/ui/mute_button.js @@ -38,6 +38,7 @@ shaka.ui.MuteButton = class extends shaka.ui.Element { this.button_.classList.add('shaka-mute-button'); this.button_.classList.add('shaka-tooltip'); this.button_.classList.add('shaka-no-propagation'); + this.button_.ariaPressed = 'false'; /** @private {!shaka.ui.Icon} */ this.icon_ = new shaka.ui.Icon(this.button_, @@ -154,6 +155,9 @@ shaka.ui.MuteButton = class extends shaka.ui.Element { } this.button_.ariaLabel = this.localization.resolve(label); + this.button_.ariaLabel = this.localization.resolve(label); + this.button_.ariaPressed = label == LocIds.UNMUTE ? 'true' : 'false'; + this.nameSpan_.textContent = this.localization.resolve(label); this.nameSpan_.textContent = this.localization.resolve(label); } diff --git a/ui/overflow_menu.js b/ui/overflow_menu.js index 3e54626b8..5f2b929b2 100644 --- a/ui/overflow_menu.js +++ b/ui/overflow_menu.js @@ -145,6 +145,7 @@ shaka.ui.OverflowMenu = class extends shaka.ui.Element { this.overflowMenu_.classList.add('shaka-no-propagation'); this.overflowMenu_.classList.add('shaka-show-controls-on-mouse-over'); this.overflowMenu_.classList.add('shaka-hidden'); + this.overflowMenu_.setAttribute('role', 'menu'); this.controlsContainer_.appendChild(this.overflowMenu_); } @@ -155,6 +156,8 @@ shaka.ui.OverflowMenu = class extends shaka.ui.Element { addOverflowMenuButton_() { /** @private {!HTMLButtonElement} */ this.overflowMenuButton_ = shaka.util.Dom.createButton(); + this.overflowMenuButton_.setAttribute('aria-haspopup', 'true'); + this.overflowMenuButton_.setAttribute('aria-expanded', 'false'); this.overflowMenuButton_.classList.add('shaka-overflow-menu-button'); this.overflowMenuButton_.classList.add('shaka-no-propagation'); this.overflowMenuButton_.classList.add('shaka-tooltip'); @@ -191,11 +194,14 @@ shaka.ui.OverflowMenu = class extends shaka.ui.Element { this.controls.hideContextMenus(); if (this.controls.anySettingsMenusAreOpen()) { this.controls.hideSettingsMenus(); + this.overflowMenuButton_.setAttribute('aria-expanded', 'false'); + this.overflowMenuButton_.focus(); } else { // Force to close any submenu. this.controls.dispatchEvent(new shaka.util.FakeEvent('submenuclose')); shaka.ui.Utils.setDisplay(this.overflowMenu_, true); + this.overflowMenuButton_.setAttribute('aria-expanded', 'true'); this.controls.computeOpacity(); // If overflow menu has currently visible buttons, focus on the diff --git a/ui/pip_button.js b/ui/pip_button.js index 53fc89997..cdf912937 100644 --- a/ui/pip_button.js +++ b/ui/pip_button.js @@ -44,6 +44,7 @@ shaka.ui.PipButton = class extends shaka.ui.Element { this.pipButton_.classList.add('shaka-pip-button'); this.pipButton_.classList.add('shaka-tooltip'); this.pipButton_.classList.add('shaka-no-propagation'); + this.pipButton_.ariaPressed = 'false'; /** @private {!shaka.ui.Icon} */ this.pipIcon_ = new shaka.ui.Icon(this.pipButton_, @@ -143,6 +144,7 @@ shaka.ui.PipButton = class extends shaka.ui.Element { this.localization.resolve(LocIds.EXIT_PICTURE_IN_PICTURE); this.currentPipState_.textContent = this.localization.resolve(LocIds.ON); + this.pipButton_.ariaPressed = 'true'; } @@ -154,6 +156,7 @@ shaka.ui.PipButton = class extends shaka.ui.Element { this.localization.resolve(LocIds.ENTER_PICTURE_IN_PICTURE); this.currentPipState_.textContent = this.localization.resolve(LocIds.OFF); + this.pipButton_.ariaPressed = 'false'; } diff --git a/ui/playback_rate_selection.js b/ui/playback_rate_selection.js index 2ec49e6c8..8e366b031 100644 --- a/ui/playback_rate_selection.js +++ b/ui/playback_rate_selection.js @@ -115,7 +115,7 @@ shaka.ui.PlaybackRateSelection = class extends shaka.ui.SettingsMenu { if (span) { const button = span.parentElement; button.appendChild(shaka.ui.Utils.checkmarkIcon()); - button.ariaSelected = 'true'; + button.setAttribute('aria-checked', 'true'); span.classList.add('shaka-chosen-item'); } @@ -132,6 +132,9 @@ shaka.ui.PlaybackRateSelection = class extends shaka.ui.SettingsMenu { addPlaybackRates_() { for (const rate of this.controls.getConfig().playbackRates) { const button = shaka.util.Dom.createButton(); + // ARIA: single-select menu item + button.setAttribute('role', 'menuitemradio'); + button.setAttribute('aria-checked', 'false'); const span = shaka.util.Dom.createHTMLElement('span'); span.textContent = rate + 'x'; button.appendChild(span); diff --git a/ui/recenter_vr.js b/ui/recenter_vr.js index dc4226b90..5b45fc28b 100644 --- a/ui/recenter_vr.js +++ b/ui/recenter_vr.js @@ -36,7 +36,6 @@ shaka.ui.RecenterVRButton = class extends shaka.ui.Element { this.recenterVRButton_ = shaka.util.Dom.createButton(); this.recenterVRButton_.classList.add('shaka-recenter-vr-button'); this.recenterVRButton_.classList.add('shaka-tooltip'); - this.recenterVRButton_.ariaPressed = 'false'; /** @private {!shaka.ui.Icon} */ this.recenterVRIcon_ = new shaka.ui.Icon(this.recenterVRButton_, diff --git a/ui/remote_button.js b/ui/remote_button.js index 8eb8a59b8..04e4c3243 100644 --- a/ui/remote_button.js +++ b/ui/remote_button.js @@ -197,6 +197,8 @@ shaka.ui.RemoteButton = class extends shaka.ui.Element { this.callbackId_ = -1; } } + this.remoteButton_.ariaPressed = + this.remote_?.state == 'connected' ? 'true' : 'false'; } /** diff --git a/ui/resolution_selection.js b/ui/resolution_selection.js index ea5590d35..8c9cf5990 100644 --- a/ui/resolution_selection.js +++ b/ui/resolution_selection.js @@ -230,6 +230,9 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { // Add the Auto button const autoButton = shaka.util.Dom.createButton(); + // ARIA: single-select menu item + autoButton.setAttribute('role', 'menuitemradio'); + autoButton.setAttribute('aria-checked', 'false'); autoButton.classList.add('shaka-enable-abr-button'); this.eventManager.listen(autoButton, 'click', () => { const config = {abr: {enabled: true}}; @@ -245,7 +248,7 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { // If abr is enabled reflect it by marking 'Auto' as selected. if (this.player.getConfiguration().abr.enabled) { - autoButton.ariaSelected = 'true'; + autoButton.setAttribute('aria-checked', 'true'); autoButton.appendChild(shaka.ui.Utils.checkmarkIcon()); this.abrOnSpan_.classList.add('shaka-chosen-item'); @@ -332,6 +335,9 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { // Add new ones for (const track of tracks) { const button = shaka.util.Dom.createButton(); + // ARIA: single-select menu item + button.setAttribute('role', 'menuitemradio'); + button.setAttribute('aria-checked', 'false'); button.classList.add('explicit-resolution'); this.eventManager.listen(button, 'click', () => this.onTrackSelected_(track)); @@ -346,7 +352,7 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { if (!abrEnabled && track == selectedTrack) { // If abr is disabled, mark the selected track's resolution. - button.ariaSelected = 'true'; + button.setAttribute('aria-checked', 'true'); button.appendChild(shaka.ui.Utils.checkmarkIcon()); span.classList.add('shaka-chosen-item'); this.currentSelection.textContent = span.textContent; @@ -405,6 +411,9 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { // Add new ones for (const track of tracks) { const button = shaka.util.Dom.createButton(); + // ARIA: single-select menu item + button.setAttribute('role', 'menuitemradio'); + button.setAttribute('aria-checked', 'false'); button.classList.add('explicit-resolution'); this.eventManager.listen(button, 'click', () => this.onVideoTrackSelected_(track)); @@ -429,7 +438,7 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { if (!abrEnabled && track == selectedTrack) { // If abr is disabled, mark the selected track's resolution. - button.ariaSelected = 'true'; + button.setAttribute('aria-checked', 'true'); button.appendChild(shaka.ui.Utils.checkmarkIcon()); span.classList.add('shaka-chosen-item'); this.currentSelection.textContent = span.textContent; diff --git a/ui/seek_bar.js b/ui/seek_bar.js index 9c575ee2f..825ca1cf2 100644 --- a/ui/seek_bar.js +++ b/ui/seek_bar.js @@ -746,6 +746,7 @@ shaka.ui.SeekBar = class extends shaka.ui.RangeElement { } this.thumbnailImage_ = /** @type {!HTMLImageElement} */ ( shaka.util.Dom.createHTMLElement('img')); + this.thumbnailImage_.alt = ''; this.thumbnailImage_.classList.add('shaka-player-ui-thumbnail-image'); this.thumbnailImage_.draggable = false; this.thumbnailImage_.src = uri; diff --git a/ui/settings_menu.js b/ui/settings_menu.js index cf5912e42..d481e7c74 100644 --- a/ui/settings_menu.js +++ b/ui/settings_menu.js @@ -88,6 +88,8 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { addButton_(iconText) { /** @protected {!HTMLButtonElement} */ this.button = shaka.util.Dom.createButton(); + this.button.setAttribute('aria-haspopup', 'true'); + this.button.setAttribute('aria-expanded', 'false'); this.button.classList.add('shaka-overflow-button'); /** @protected {!shaka.ui.Icon}*/ @@ -118,6 +120,7 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { this.menu = shaka.util.Dom.createHTMLElement('div'); this.menu.classList.add('shaka-no-propagation'); this.menu.classList.add('shaka-show-controls-on-mouse-over'); + this.menu.setAttribute('role', 'menu'); if (this.isSubMenu) { this.menu.classList.add('shaka-sub-menu'); } else { @@ -131,6 +134,7 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { this.menu.appendChild(this.backButton); this.eventManager.listen(this.backButton, 'click', () => { this.controls.hideSettingsMenus(); + this.backButton.focus(); }); /** @private {shaka.ui.Icon} */ @@ -195,6 +199,7 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { onButtonClick_() { if (!this.parent.classList.contains('shaka-context-menu')) { this.controls.hideContextMenus(); + this.button.setAttribute('aria-expanded', 'false'); } if (!this.isSubMenu && this.controls.anySettingsMenusAreOpen()) { this.controls.hideSettingsMenus(); @@ -206,8 +211,11 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { shaka.ui.Utils.setDisplay(this.menu, true); shaka.ui.Utils.focusOnTheChosenItem(this.menu); this.adjustCustomStyle_(); + this.button.setAttribute('aria-expanded', 'true'); } else { shaka.ui.Utils.setDisplay(this.menu, false); + this.button.setAttribute('aria-expanded', 'false'); + this.button.focus(); } } } diff --git a/ui/skip_ad_button.js b/ui/skip_ad_button.js index e4b87c558..145d8276d 100644 --- a/ui/skip_ad_button.js +++ b/ui/skip_ad_button.js @@ -118,7 +118,8 @@ shaka.ui.SkipAdButton = class extends shaka.ui.Element { * @private */ updateAriaLabel_() { - // TODO + const LocIds = shaka.ui.Locales.Ids; + this.button_.ariaLabel = this.localization.resolve(LocIds.SKIP_AD); } /** diff --git a/ui/statistics_button.js b/ui/statistics_button.js index 1dc5e0389..eebb2cc52 100644 --- a/ui/statistics_button.js +++ b/ui/statistics_button.js @@ -37,6 +37,9 @@ shaka.ui.StatisticsButton = class extends shaka.ui.Element { /** @private {!HTMLButtonElement} */ this.button_ = shaka.util.Dom.createButton(); this.button_.classList.add('shaka-statistics-button'); + this.button_.classList.add('shaka-tooltip'); + this.button_.classList.add('shaka-no-propagation'); + this.button_.ariaPressed = 'false'; /** @private {!shaka.ui.Icon} */ this.icon_ = new shaka.ui.Icon(this.button_, @@ -207,10 +210,12 @@ shaka.ui.StatisticsButton = class extends shaka.ui.Element { this.icon_.use(shaka.ui.Enums.MaterialDesignSVGIcons['STATISTICS_OFF']); this.timer_.tickEvery(0.1); shaka.ui.Utils.setDisplay(this.container_, true); + this.button_.ariaPressed = 'true'; } else { this.icon_.use(shaka.ui.Enums.MaterialDesignSVGIcons['STATISTICS_ON']); this.timer_.stop(); shaka.ui.Utils.setDisplay(this.container_, false); + this.button_.ariaPressed = 'false'; } } diff --git a/ui/text_position.js b/ui/text_position.js index 13076b0b8..508409992 100644 --- a/ui/text_position.js +++ b/ui/text_position.js @@ -123,6 +123,9 @@ shaka.ui.TextPosition = class extends shaka.ui.SettingsMenu { // 4. Add new items for (const position of Object.values(shaka.config.PositionArea)) { const button = shaka.util.Dom.createButton(); + // ARIA: single-select menu item + button.setAttribute('role', 'menuitemradio'); + button.setAttribute('aria-checked', 'false'); const span = shaka.util.Dom.createHTMLElement('span'); span.textContent = this.getNameOfPosition_(position); button.appendChild(span); @@ -145,7 +148,7 @@ shaka.ui.TextPosition = class extends shaka.ui.SettingsMenu { this.menu, 'shaka-ui-icon shaka-chosen-item'); if (checkmarkIcon) { const previouslySelectedButton = checkmarkIcon.parentElement; - previouslySelectedButton.removeAttribute('aria-selected'); + previouslySelectedButton.setAttribute('aria-checked', 'false'); const previouslySelectedSpan = previouslySelectedButton.getElementsByTagName('span')[0]; if (previouslySelectedSpan) { @@ -164,7 +167,7 @@ shaka.ui.TextPosition = class extends shaka.ui.SettingsMenu { if (span) { const button = span.parentElement; button.appendChild(shaka.ui.Utils.checkmarkIcon()); - button.ariaSelected = 'true'; + button.setAttribute('aria-checked', 'true'); span.classList.add('shaka-chosen-item'); } this.currentSelection.textContent = positionAreaName; diff --git a/ui/text_selection.js b/ui/text_selection.js index d4635abd4..a8e2fa5e7 100644 --- a/ui/text_selection.js +++ b/ui/text_selection.js @@ -107,7 +107,9 @@ shaka.ui.TextSelection = class extends shaka.ui.SettingsMenu { */ addOffOption_() { const off = shaka.util.Dom.createButton(); - off.ariaSelected = 'true'; + // ARIA: single-select menu item + off.setAttribute('role', 'menuitemradio'); + off.setAttribute('aria-checked', 'true'); this.menu.appendChild(off); off.appendChild(shaka.ui.Utils.checkmarkIcon()); @@ -164,7 +166,9 @@ shaka.ui.TextSelection = class extends shaka.ui.SettingsMenu { this.menu.appendChild(offButton); if (!hasTrack) { - offButton.ariaSelected = 'true'; + // ARIA: single-select menu item + offButton.setAttribute('role', 'menuitemradio'); + offButton.setAttribute('aria-checked', 'true'); offButton.appendChild(shaka.ui.Utils.checkmarkIcon()); this.captionsOffSpan_.classList.add('shaka-chosen-item'); this.currentSelection.textContent = diff --git a/ui/text_size.js b/ui/text_size.js index 57f5343b5..482fb55a1 100644 --- a/ui/text_size.js +++ b/ui/text_size.js @@ -121,6 +121,9 @@ shaka.ui.TextSize = class extends shaka.ui.SettingsMenu { for (const fontScaleFactor of this.controls.getConfig().captionsFontScaleFactors) { const button = shaka.util.Dom.createButton(); + // ARIA: single-select menu item + button.setAttribute('role', 'menuitemradio'); + button.setAttribute('aria-checked', 'false'); const span = shaka.util.Dom.createHTMLElement('span'); span.textContent = fontScaleFactor * 100 + '%'; button.appendChild(span); @@ -143,7 +146,7 @@ shaka.ui.TextSize = class extends shaka.ui.SettingsMenu { this.menu, 'shaka-ui-icon shaka-chosen-item'); if (checkmarkIcon) { const previouslySelectedButton = checkmarkIcon.parentElement; - previouslySelectedButton.removeAttribute('aria-selected'); + previouslySelectedButton.setAttribute('aria-checked', 'false'); const previouslySelectedSpan = previouslySelectedButton.getElementsByTagName('span')[0]; if (previouslySelectedSpan) { @@ -162,7 +165,7 @@ shaka.ui.TextSize = class extends shaka.ui.SettingsMenu { if (span) { const button = span.parentElement; button.appendChild(shaka.ui.Utils.checkmarkIcon()); - button.ariaSelected = 'true'; + button.setAttribute('aria-checked', 'true'); span.classList.add('shaka-chosen-item'); } this.currentSelection.textContent = fontScaleFactorName; diff --git a/ui/toggle_stereoscopic.js b/ui/toggle_stereoscopic.js index ce928ee38..e7e0e07d3 100644 --- a/ui/toggle_stereoscopic.js +++ b/ui/toggle_stereoscopic.js @@ -77,6 +77,8 @@ shaka.ui.ToggleStereoscopicButton = class extends shaka.ui.Element { return; } vr.toggleStereoscopicMode(); + this.toggleStereoscopicButton_.ariaPressed = + vr.isStereoscopicModeEnabled() ? 'true' : 'false'; }); this.eventManager.listen(vr, 'vrstatuschanged', () => { diff --git a/ui/ui_utils.js b/ui/ui_utils.js index 053a57d16..e1b9b7293 100644 --- a/ui/ui_utils.js +++ b/ui/ui_utils.js @@ -70,8 +70,7 @@ shaka.ui.Utils = class { shaka.ui.Enums.MaterialDesignSVGIcons['CHECKMARK']); const iconElement = icon.getSvgElement(); iconElement.classList.add('shaka-chosen-item'); - // Screen reader should ignore icon text. - iconElement.ariaHidden = 'true'; + return iconElement; } diff --git a/ui/video_type_selection.js b/ui/video_type_selection.js index c838750fa..990b771f0 100644 --- a/ui/video_type_selection.js +++ b/ui/video_type_selection.js @@ -120,6 +120,9 @@ shaka.ui.VideoTypeSelection = class extends shaka.ui.SettingsMenu { if (roles.size > 1) { for (const role of roles) { const button = shaka.util.Dom.createButton(); + // ARIA: single-select menu item + button.setAttribute('role', 'menuitemradio'); + button.setAttribute('aria-checked', 'false'); this.eventManager.listen(button, 'click', () => this.onVideoRoleSelected_(role)); @@ -128,7 +131,7 @@ shaka.ui.VideoTypeSelection = class extends shaka.ui.SettingsMenu { button.appendChild(span); if (selectedTrack.roles.includes(role)) { - button.ariaSelected = 'true'; + button.setAttribute('aria-checked', 'true'); button.appendChild(shaka.ui.Utils.checkmarkIcon()); span.classList.add('shaka-chosen-item'); this.currentSelection.textContent = span.textContent; diff --git a/ui/watermark.js b/ui/watermark.js index e6237a772..61f65e3e9 100644 --- a/ui/watermark.js +++ b/ui/watermark.js @@ -38,6 +38,7 @@ shaka.ui.Watermark = class extends shaka.ui.Element { this.canvas_.style.height = '100%'; this.canvas_.style.zIndex = '2'; this.canvas_.style.pointerEvents = 'none'; + this.canvas_.setAttribute('aria-hidden', 'true'); this.parent.appendChild(this.canvas_); this.resizeCanvas_();