diff --git a/docs/tutorials/ui-customization.md b/docs/tutorials/ui-customization.md index d54e2d604d..4c563a4292 100644 --- a/docs/tutorials/ui-customization.md +++ b/docs/tutorials/ui-customization.md @@ -67,6 +67,10 @@ The following elements can be added to the UI bar using this configuration value supports AirPlay. * cast: adds a button that opens a Chromecast dialog. The button is visible only if there is at least one Chromecast device on the same network available for casting. +* quality: adds a button that controls enabling/disabling of abr and video resolution selection. +* language: adds a button that controls audio language selection. +* playback_rate: adds a button that controls the playback rate selection. +* captions: adds a button that controls the current text track selection (including turning it off). Similarly, the 'overflowMenuButtons' configuration option can be used to control diff --git a/test/ui/ui_unit.js b/test/ui/ui_unit.js index ea52cd7f75..6755598968 100644 --- a/test/ui/ui_unit.js +++ b/test/ui/ui_unit.js @@ -409,6 +409,80 @@ describe('UI', () => { }); }); + describe('control panel buttons with submenus', () => { + /** @type {!HTMLElement} */ + let resolutionMenu; + /** @type {!Element} */ + let resolutionMenuButton; + /** @type {!HTMLElement} */ + let languageMenu; + /** @type {!Element} */ + let languageMenuButton; + + beforeEach(() => { + const config = { + controlPanelElements: [ + 'quality', + 'language', + ], + }; + const ui = UiUtils.createUIThroughAPI(videoContainer, video, config); + player = ui.getControls().getLocalPlayer(); + + const resolutionsMenus = + videoContainer.getElementsByClassName('shaka-resolutions'); + expect(resolutionsMenus.length).toBe(1); + resolutionMenu = /** @type {!HTMLElement} */ (resolutionsMenus[0]); + + const resolutionMenuButtons = + videoContainer.getElementsByClassName('shaka-resolution-button'); + expect(resolutionMenuButtons.length).toBe(1); + resolutionMenuButton = resolutionMenuButtons[0]; + + const languageMenus = + videoContainer.getElementsByClassName('shaka-audio-languages'); + expect(languageMenus.length).toBe(1); + languageMenu = /** @type {!HTMLElement} */ (languageMenus[0]); + + const languageMenuButtons = + videoContainer.getElementsByClassName('shaka-language-button'); + expect(languageMenuButtons.length).toBe(1); + languageMenuButton = languageMenuButtons[0]; + }); + + it('menus are initially hidden', () => { + expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(true); + expect(languageMenu.classList.contains('shaka-hidden')).toBe(true); + }); + + it('a menu becomes visible if the button is clicked', () => { + resolutionMenuButton.click(); + + expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(false); + }); + + it('a menu becomes hidden if the "close" button is clicked', () => { + resolutionMenuButton.click(); + + const backToOverflowButtons = + videoContainer.getElementsByClassName('shaka-back-to-overflow-button'); + expect(backToOverflowButtons.length).toBe(2); + const backToOverflowButton = + /** @type {!HTMLElement} */ (backToOverflowButtons[0]); + backToOverflowButton.click(); + + expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(true); + }); + + it('a menu becomes hidden if another one is opened', () => { + resolutionMenuButton.click(); + languageMenuButton.click(); + + expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(true); + expect(languageMenu.classList.contains('shaka-hidden')).toBe(false); + }); + }); + describe('resolutions menu', () => { /** @type {!HTMLElement} */ let resolutionsMenu; diff --git a/ui/audio_language_selection.js b/ui/audio_language_selection.js index 638d673a84..0416bf8e6d 100644 --- a/ui/audio_language_selection.js +++ b/ui/audio_language_selection.js @@ -7,6 +7,7 @@ goog.provide('shaka.ui.AudioLanguageSelection'); +goog.require('shaka.ui.Controls'); goog.require('shaka.ui.Enums'); goog.require('shaka.ui.LanguageUtils'); goog.require('shaka.ui.Locales'); @@ -118,3 +119,6 @@ shaka.ui.AudioLanguageSelection.Factory = class { shaka.ui.OverflowMenu.registerElement( 'language', new shaka.ui.AudioLanguageSelection.Factory()); + +shaka.ui.Controls.registerElement( + 'language', new shaka.ui.AudioLanguageSelection.Factory()); diff --git a/ui/controls.js b/ui/controls.js index 2e091ff232..7660ccaf20 100644 --- a/ui/controls.js +++ b/ui/controls.js @@ -776,8 +776,12 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { // on the page. The click event listener on window ensures that. // However, clicks on the bottom controls don't propagate to the container, // so we have to explicitly hide the menus onclick here. - this.eventManager_.listen(this.bottomControls_, 'click', () => { - this.hideSettingsMenus(); + this.eventManager_.listen(this.bottomControls_, 'click', (e) => { + // We explicitly deny this measure when clicking on buttons that + // open submenus in the control panel. + if (!e.target['closest']('.shaka-overflow-button')) { + this.hideSettingsMenus(); + } }); this.addAdControls_(); @@ -891,6 +895,12 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { // Listen for click events to dismiss the settings menus. this.eventManager_.listen(window, 'click', () => this.hideSettingsMenus()); + // Avoid having multiple submenus open at the same time. + this.eventManager_.listen( + this, 'submenuopen', () => { + this.hideSettingsMenus(); + }); + this.eventManager_.listen(this.video_, 'play', () => { this.onPlayStateChange_(); }); diff --git a/ui/enums.js b/ui/enums.js index fcf738dadf..29c1c6b210 100644 --- a/ui/enums.js +++ b/ui/enums.js @@ -16,7 +16,9 @@ goog.provide('shaka.ui.Enums'); shaka.ui.Enums.MaterialDesignIcons = { 'FULLSCREEN': 'fullscreen', 'EXIT_FULLSCREEN': 'fullscreen_exit', + 'CLOSE': 'close', 'CLOSED_CAPTIONS': 'closed_caption', + 'CLOSED_CAPTIONS_OFF': 'closed_caption_disabled', 'CHECKMARK': 'done', 'LANGUAGE': 'language', 'PIP': 'picture_in_picture_alt', diff --git a/ui/less/overflow_menu.less b/ui/less/overflow_menu.less index af1daece24..20bae9059b 100644 --- a/ui/less/overflow_menu.less +++ b/ui/less/overflow_menu.less @@ -141,13 +141,3 @@ /* TODO(b/116651454): eliminate hard-coded offsets */ left: 17px; } - -/* The captions button, when captions are on. */ -.shaka-captions-on { - color: black; -} - -/* The captions button, when captions are off. */ -.shaka-captions-off { - color: grey; -} diff --git a/ui/overflow_menu.js b/ui/overflow_menu.js index e93b6c3df5..2b497022c3 100644 --- a/ui/overflow_menu.js +++ b/ui/overflow_menu.js @@ -48,29 +48,6 @@ shaka.ui.OverflowMenu = class extends shaka.ui.Element { this.createChildren_(); - - const backToOverflowMenuButtons = - this.controls.getVideoContainer().getElementsByClassName( - 'shaka-back-to-overflow-button'); - - for (const button of backToOverflowMenuButtons) { - this.eventManager.listen(button, 'click', () => { - // Hide the submenus, display the overflow menu - this.controls.hideSettingsMenus(); - shaka.ui.Utils.setDisplay(this.overflowMenu_, true); - - // If there are back to overflow menu buttons, there must be - // overflow menu buttons, but oh well - if (this.overflowMenu_.childNodes.length) { - /** @type {!HTMLElement} */ (this.overflowMenu_.childNodes[0]) - .focus(); - } - - // Make sure controls are displayed - this.controls.computeOpacity(); - }); - } - this.eventManager.listen( this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => { this.updateAriaLabel_(); diff --git a/ui/playback_rate_selection.js b/ui/playback_rate_selection.js index 64fefbb329..6c0bd30130 100644 --- a/ui/playback_rate_selection.js +++ b/ui/playback_rate_selection.js @@ -7,6 +7,7 @@ goog.provide('shaka.ui.PlaybackRateSelection'); +goog.require('shaka.ui.Controls'); goog.require('shaka.ui.Enums'); goog.require('shaka.ui.Locales'); goog.require('shaka.ui.Localization'); @@ -143,3 +144,6 @@ shaka.ui.PlaybackRateSelection.Factory = class { shaka.ui.OverflowMenu.registerElement( 'playback_rate', new shaka.ui.PlaybackRateSelection.Factory()); + +shaka.ui.Controls.registerElement( + 'playback_rate', new shaka.ui.PlaybackRateSelection.Factory()); diff --git a/ui/resolution_selection.js b/ui/resolution_selection.js index fe76e16238..a4d20b2826 100644 --- a/ui/resolution_selection.js +++ b/ui/resolution_selection.js @@ -8,6 +8,7 @@ goog.provide('shaka.ui.ResolutionSelection'); goog.require('goog.asserts'); +goog.require('shaka.ui.Controls'); goog.require('shaka.ui.Enums'); goog.require('shaka.ui.Locales'); goog.require('shaka.ui.Localization'); @@ -227,3 +228,6 @@ shaka.ui.ResolutionSelection.Factory = class { shaka.ui.OverflowMenu.registerElement( 'quality', new shaka.ui.ResolutionSelection.Factory()); + +shaka.ui.Controls.registerElement( + 'quality', new shaka.ui.ResolutionSelection.Factory()); diff --git a/ui/settings_menu.js b/ui/settings_menu.js index c40388a776..7d8b36a9a4 100644 --- a/ui/settings_menu.js +++ b/ui/settings_menu.js @@ -33,6 +33,8 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { this.addMenu_(); + this.inOverflowMenu_(); + this.eventManager.listen(this.button, 'click', () => { this.onButtonClick_(); }); @@ -46,6 +48,7 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { addButton_(iconText) { /** @protected {!HTMLButtonElement} */ this.button = shaka.util.Dom.createButton(); + this.button.classList.add('shaka-overflow-button'); /** @protected {!HTMLElement}*/ this.icon = shaka.util.Dom.createHTMLElement('i'); @@ -55,6 +58,7 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { const label = shaka.util.Dom.createHTMLElement('label'); label.classList.add('shaka-overflow-button-label'); + label.classList.add('shaka-overflow-menu-only'); /** @protected {!HTMLElement}*/ this.nameSpan = shaka.util.Dom.createHTMLElement('span'); @@ -83,10 +87,13 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { this.backButton = shaka.util.Dom.createButton(); this.backButton.classList.add('shaka-back-to-overflow-button'); this.menu.appendChild(this.backButton); + this.eventManager.listen(this.backButton, 'click', () => { + this.controls.hideSettingsMenus(); + }); const backIcon = shaka.util.Dom.createHTMLElement('i'); backIcon.classList.add('material-icons-round'); - backIcon.textContent = shaka.ui.Enums.MaterialDesignIcons.BACK; + backIcon.textContent = shaka.ui.Enums.MaterialDesignIcons.CLOSE; this.backButton.appendChild(backIcon); /** @protected {!HTMLElement}*/ @@ -97,11 +104,36 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { controlsContainer.appendChild(this.menu); } + /** @private */ + inOverflowMenu_() { + // Initially, submenus are created with a "Close" option. When present + // inside of the overflow menu, that option must be replaced with a + // "Back" arrow that returns the user to the main menu. + if (this.parent.classList.contains('shaka-overflow-menu')) { + this.backButton.firstChild.textContent = + shaka.ui.Enums.MaterialDesignIcons.BACK; + + this.eventManager.listen(this.backButton, 'click', () => { + shaka.ui.Utils.setDisplay(this.parent, true); + + /** @type {!HTMLElement} */ + (this.parent.childNodes[0]).focus(); + + // Make sure controls are displayed + this.controls.computeOpacity(); + }); + } + } + /** @private */ onButtonClick_() { - this.controls.dispatchEvent(new shaka.util.FakeEvent('submenuopen')); - shaka.ui.Utils.setDisplay(this.menu, true); - shaka.ui.Utils.focusOnTheChosenItem(this.menu); + if (this.menu.classList.contains('shaka-hidden')) { + this.controls.dispatchEvent(new shaka.util.FakeEvent('submenuopen')); + shaka.ui.Utils.setDisplay(this.menu, true); + shaka.ui.Utils.focusOnTheChosenItem(this.menu); + } else { + shaka.ui.Utils.setDisplay(this.menu, false); + } } }; diff --git a/ui/text_selection.js b/ui/text_selection.js index aeb3580c89..22506549cc 100644 --- a/ui/text_selection.js +++ b/ui/text_selection.js @@ -7,6 +7,7 @@ goog.provide('shaka.ui.TextSelection'); +goog.require('shaka.ui.Controls'); goog.require('shaka.ui.Enums'); goog.require('shaka.ui.LanguageUtils'); goog.require('shaka.ui.Locales'); @@ -107,12 +108,12 @@ shaka.ui.TextSelection = class extends shaka.ui.SettingsMenu { /** @private */ onCaptionStateChange_() { if (this.player.isTextTrackVisible()) { - this.icon.classList.add('shaka-captions-on'); - this.icon.classList.remove('shaka-captions-off'); + this.icon.textContent = + shaka.ui.Enums.MaterialDesignIcons.CLOSED_CAPTIONS_OFF; this.button.ariaPressed = 'true'; } else { - this.icon.classList.add('shaka-captions-off'); - this.icon.classList.remove('shaka-captions-on'); + this.icon.textContent = + shaka.ui.Enums.MaterialDesignIcons.CLOSED_CAPTIONS; this.button.ariaPressed = 'false'; } @@ -216,3 +217,6 @@ shaka.ui.TextSelection.Factory = class { shaka.ui.OverflowMenu.registerElement( 'captions', new shaka.ui.TextSelection.Factory()); + +shaka.ui.Controls.registerElement( + 'captions', new shaka.ui.TextSelection.Factory());