diff --git a/demo/visualizer_button.js b/demo/visualizer_button.js index 558cbbe71..94e8398c7 100644 --- a/demo/visualizer_button.js +++ b/demo/visualizer_button.js @@ -65,14 +65,23 @@ shakaDemo.VisualizerButton = class extends shaka.ui.Element { shakaDemoMain.getIsVisualizerActive()) { shakaDemoMain.setIsVisualizerActive(false); } - this.setDisplay_(!this.castProxy_.isCasting()); + this.checkAvailability_(); }); this.eventManager.listen(document, 'fullscreenchange', () => { - this.setDisplay_(!this.controls.isFullScreenEnabled()); + this.checkAvailability_(); }); - this.setDisplay_(!this.controls.isFullScreenEnabled()); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.checkAvailability_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.checkAvailability_(); + }); + } + + this.checkAvailability_(); } /** @private */ @@ -88,11 +97,12 @@ shakaDemo.VisualizerButton = class extends shaka.ui.Element { /** - * @param {boolean} display * @private */ - setDisplay_(display) { - if (display) { + checkAvailability_() { + if (!this.castProxy_.isCasting() && + !this.controls.isFullScreenEnabled() && + !this.isSubMenuOpened) { // Removing a non-existent class doesn't throw, so, even if // the element is not hidden, this should be fine. this.button_.classList.remove('shaka-hidden'); diff --git a/project-words.txt b/project-words.txt index 9f8a7b6ce..25bdcd2ee 100644 --- a/project-words.txt +++ b/project-words.txt @@ -77,6 +77,7 @@ spacebar stalldetected startstreaming statechanged +submenuclose submenuopen textchanged textlang diff --git a/test/ui/ui_unit.js b/test/ui/ui_unit.js index 525257d8f..041b20e8a 100644 --- a/test/ui/ui_unit.js +++ b/test/ui/ui_unit.js @@ -377,9 +377,11 @@ describe('UI', () => { }); it('is accessible', () => { - for (const button of overflowMenu.childNodes) { - expect(/** @type {!HTMLElement} */ (button) - .hasAttribute('aria-label')).toBe(true); + for (const node of overflowMenu.childNodes) { + const element = /** @type {!HTMLElement} */ (node); + if (element.tagName.toLowerCase() == 'button') { + expect(element.hasAttribute('aria-label')).toBe(true); + } } }); }); @@ -577,6 +579,11 @@ describe('UI', () => { resolutionMenuButton.click(); languageMenuButton.click(); + expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(true); + expect(languageMenu.classList.contains('shaka-hidden')).toBe(true); + + languageMenuButton.click(); + expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(true); expect(languageMenu.classList.contains('shaka-hidden')).toBe(false); }); diff --git a/ui/ad_statistics_button.js b/ui/ad_statistics_button.js index c7e37a9d4..df5c674d8 100644 --- a/ui/ad_statistics_button.js +++ b/ui/ad_statistics_button.js @@ -141,6 +141,15 @@ shaka.ui.AdStatisticsButton = class extends shaka.ui.Element { this.adManager, shaka.ads.Utils.AD_STARTED, () => { shaka.ui.Utils.setDisplay(this.button_, true); }); + + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + shaka.ui.Utils.setDisplay(this.button_, false); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + shaka.ui.Utils.setDisplay(this.button_, true); + }); + } } /** @private */ diff --git a/ui/audio_language_selection.js b/ui/audio_language_selection.js index c3d72faba..4e9205b3c 100644 --- a/ui/audio_language_selection.js +++ b/ui/audio_language_selection.js @@ -62,6 +62,15 @@ shaka.ui.AudioLanguageSelection = class extends shaka.ui.SettingsMenu { this.onAudioTracksChanged_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.onAudioTracksChanged_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.onAudioTracksChanged_(); + }); + } + // Set up all the strings in the user's preferred language. this.updateLocalizedStrings_(); @@ -85,7 +94,8 @@ shaka.ui.AudioLanguageSelection = class extends shaka.ui.SettingsMenu { this.button.setAttribute('shaka-status', this.currentSelection.innerText); const numberOfItems = this.menu.getElementsByTagName('button').length; - shaka.ui.Utils.setDisplay(this.button, numberOfItems > 2); + shaka.ui.Utils.setDisplay( + this.button, numberOfItems > 2 && !this.isSubMenuOpened); } /** diff --git a/ui/cast_button.js b/ui/cast_button.js index 3908d4d9e..5f0871440 100644 --- a/ui/cast_button.js +++ b/ui/cast_button.js @@ -90,6 +90,15 @@ shaka.ui.CastButton = class extends shaka.ui.Element { this.eventManager.listen(this.controls, 'caststatuschanged', () => { this.onCastStatusChange_(); }); + + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + shaka.ui.Utils.setDisplay(this.castButton_, false); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + shaka.ui.Utils.setDisplay(this.castButton_, true); + }); + } } diff --git a/ui/chapter_selection.js b/ui/chapter_selection.js index 2a3fe56eb..c25a7c21e 100644 --- a/ui/chapter_selection.js +++ b/ui/chapter_selection.js @@ -54,6 +54,15 @@ shaka.ui.ChapterSelection = class extends shaka.ui.SettingsMenu { this.updateChapters_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.updateChapters_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.updateChapters_(); + }); + } + // Set up all the strings in the user's preferred language. this.updateLocalizedStrings_(); @@ -104,7 +113,7 @@ shaka.ui.ChapterSelection = class extends shaka.ui.SettingsMenu { this.menu.appendChild(button); } - shaka.ui.Utils.setDisplay(this.button, true); + shaka.ui.Utils.setDisplay(this.button, !this.isSubMenuOpened); shaka.ui.Utils.focusOnTheChosenItem(this.menu); } else { shaka.ui.Utils.setDisplay(this.button, false); diff --git a/ui/controls.js b/ui/controls.js index 9fb751c7e..a33324cb4 100644 --- a/ui/controls.js +++ b/ui/controls.js @@ -60,6 +60,16 @@ goog.requireType('shaka.cast.CastReceiver'); */ +/** + * @event shaka.ui.Controls.SubMenuCloseEvent + * @description Fired when one of the overflow submenus is closed + * (e. g. language/resolution/subtitle selection). + * @property {string} type + * 'submenuclose' + * @exportDoc + */ + + /** * @event shaka.ui.Controls.CaptionSelectionUpdatedEvent * @description Fired when the captions/subtitles menu has finished updating. @@ -1521,12 +1531,6 @@ 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/copy_video_frame_button.js b/ui/copy_video_frame_button.js index 5c9478586..52136fda6 100644 --- a/ui/copy_video_frame_button.js +++ b/ui/copy_video_frame_button.js @@ -119,6 +119,15 @@ shaka.ui.CopyVideoFrameButton = class extends shaka.ui.Element { this.checkAvailability_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.checkAvailability_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.checkAvailability_(); + }); + } + this.checkAvailability_(); } @@ -127,8 +136,8 @@ shaka.ui.CopyVideoFrameButton = class extends shaka.ui.Element { * @private */ checkAvailability_() { - shaka.ui.Utils.setDisplay( - this.button_, this.controls.canCopyVideoFrameToClipboard()); + shaka.ui.Utils.setDisplay(this.button_, + this.controls.canCopyVideoFrameToClipboard() && !this.isSubMenuOpened); } diff --git a/ui/element.js b/ui/element.js index 5ed6f3a83..8ba0fc015 100644 --- a/ui/element.js +++ b/ui/element.js @@ -71,7 +71,7 @@ shaka.ui.Element = class { * @protected {?shaka.extern.IAd} * @exportInterface */ - this.ad = this.adManager.getCurrentAd(); ; + this.ad = this.adManager.getCurrentAd(); const AD_STARTED = shaka.ads.Utils.AD_STARTED; this.eventManager.listen(this.adManager, AD_STARTED, () => { @@ -82,6 +82,27 @@ shaka.ui.Element = class { this.eventManager.listen(this.adManager, AD_STOPPED, () => { this.ad = null; }); + + /** + * @protected {boolean} + * @exportInterface + */ + this.isSubMenu = this.parent.classList.contains('shaka-overflow-menu'); + + /** + * @protected {boolean} + * @exportInterface + */ + this.isSubMenuOpened = false; + + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.isSubMenuOpened = true; + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.isSubMenuOpened = false; + }); + } } /** diff --git a/ui/externs/ui.js b/ui/externs/ui.js index 7a8f579da..fdfaed090 100644 --- a/ui/externs/ui.js +++ b/ui/externs/ui.js @@ -607,6 +607,18 @@ shaka.extern.IUIElement = class { * @exportDoc */ this.ad; + + /** + * @protected {boolean} + * @exportInterface + */ + this.isSubMenu; + + /** + * @protected {boolean} + * @exportInterface + */ + this.isSubMenuOpened; } /** diff --git a/ui/less/overflow_menu.less b/ui/less/overflow_menu.less index 837855672..db1d537bd 100644 --- a/ui/less/overflow_menu.less +++ b/ui/less/overflow_menu.less @@ -4,6 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ +.shaka-sub-menu { + /* Where the menu appears. */ + position: relative; + + /* When displayed as a flex container, elements inside will flow in a + * vertical column. */ + display: flex; + flex-direction: column; + align-items: stretch; +} + /* The overflow menu and all settings submenus. These appear on top of all * other controls (Z axis) when the overflow button is clicked. */ .shaka-overflow-menu, @@ -102,6 +113,10 @@ text-align: left; } +.shaka-overflow-button { + position: relative; +} + /* This contains span elements with single lines of text, and appears to the * right of MD icons. */ .shaka-overflow-button-label { @@ -175,7 +190,8 @@ } /* The submenus have somewhat different margins inside them. */ -.shaka-settings-menu { +.shaka-settings-menu, +.shaka-sub-menu { span { /* TODO(b/116651454): eliminate hard-coded offsets */ margin-left: 28px; diff --git a/ui/loop_button.js b/ui/loop_button.js index 9b1dcc28a..b8a7afc5e 100644 --- a/ui/loop_button.js +++ b/ui/loop_button.js @@ -120,6 +120,17 @@ shaka.ui.LoopButton = class extends shaka.ui.Element { this.eventManager.listen(this.video, 'durationchange', () => { this.checkAvailability_(); }); + + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.checkAvailability_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.checkAvailability_(); + }); + } + + this.checkAvailability_(); } /** @@ -178,7 +189,8 @@ shaka.ui.LoopButton = class extends shaka.ui.Element { * @private */ checkAvailability_() { - shaka.ui.Utils.setDisplay(this.button_, !this.player.isLive()); + shaka.ui.Utils.setDisplay( + this.button_, !this.player.isLive() && !this.isSubMenuOpened); } }; diff --git a/ui/overflow_menu.js b/ui/overflow_menu.js index e7d193572..fb3661158 100644 --- a/ui/overflow_menu.js +++ b/ui/overflow_menu.js @@ -18,6 +18,7 @@ goog.require('shaka.ui.Locales'); goog.require('shaka.ui.Localization'); goog.require('shaka.ui.Utils'); goog.require('shaka.util.Dom'); +goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.Iterables'); @@ -74,15 +75,6 @@ shaka.ui.OverflowMenu = class extends shaka.ui.Element { shaka.ui.Utils.setDisplay(this.overflowMenuButton_, true); }); - - this.eventManager.listen( - this.controls, 'submenuopen', () => { - // Hide the main overflow menu if one of the sub menus has - // been opened. - shaka.ui.Utils.setDisplay(this.overflowMenu_, false); - }); - - this.eventManager.listen( this.overflowMenu_, 'touchstart', (event) => { this.controls.setLastTouchEventTime(Date.now()); @@ -106,7 +98,7 @@ shaka.ui.OverflowMenu = class extends shaka.ui.Element { /** @private {ResizeObserver} */ this.resizeObserver_ = null; - const resize = () => this.computeMaxHeight_(); + const resize = () => this.adjustCustomStyle_(); // Use ResizeObserver if available, fallback to window resize event if (window.ResizeObserver) { @@ -201,6 +193,9 @@ shaka.ui.OverflowMenu = class extends shaka.ui.Element { if (this.controls.anySettingsMenusAreOpen()) { this.controls.hideSettingsMenus(); } else { + // Force to close any submenu. + this.controls.dispatchEvent(new shaka.util.FakeEvent('submenuclose')); + shaka.ui.Utils.setDisplay(this.overflowMenu_, true); this.controls.computeOpacity(); @@ -216,7 +211,7 @@ shaka.ui.OverflowMenu = class extends shaka.ui.Element { Iterables.filter(this.overflowMenu_.childNodes, isDisplayed); /** @type {!HTMLElement} */ (visibleElements[0]).focus(); } - this.computeMaxHeight_(); + this.adjustCustomStyle_(); } } @@ -234,7 +229,7 @@ shaka.ui.OverflowMenu = class extends shaka.ui.Element { /** * @private */ - computeMaxHeight_() { + adjustCustomStyle_() { const rectMenu = this.overflowMenu_.getBoundingClientRect(); const styleMenu = window.getComputedStyle(this.overflowMenu_); const paddingTop = parseFloat(styleMenu.paddingTop); diff --git a/ui/pip_button.js b/ui/pip_button.js index c11eb5088..30d036ed6 100644 --- a/ui/pip_button.js +++ b/ui/pip_button.js @@ -102,13 +102,22 @@ shaka.ui.PipButton = class extends shaka.ui.Element { }); this.eventManager.listen(this.controls, 'caststatuschanged', () => { - this.onTracksChanged_(); + this.checkAvailability_(); }); this.eventManager.listen(this.player, 'trackschanged', () => { - this.onTracksChanged_(); + this.checkAvailability_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.checkAvailability_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.checkAvailability_(); + }); + } + if ('documentPictureInPicture' in window) { this.eventManager.listen(window.documentPictureInPicture, 'enter', (e) => { @@ -121,6 +130,8 @@ shaka.ui.PipButton = class extends shaka.ui.Element { }); }); } + + this.checkAvailability_(); } /** @private */ @@ -175,7 +186,7 @@ shaka.ui.PipButton = class extends shaka.ui.Element { * @return {!Promise} * @private */ - async onTracksChanged_() { + async checkAvailability_() { if (!this.controls.isPiPAllowed()) { shaka.ui.Utils.setDisplay(this.pipButton_, false); if (this.controls.isPiPEnabled()) { @@ -187,7 +198,7 @@ shaka.ui.PipButton = class extends shaka.ui.Element { await this.controls.togglePiP(); } } else { - shaka.ui.Utils.setDisplay(this.pipButton_, true); + shaka.ui.Utils.setDisplay(this.pipButton_, !this.isSubMenuOpened); } } }; diff --git a/ui/playback_rate_selection.js b/ui/playback_rate_selection.js index 2cc44094a..da1a2430e 100644 --- a/ui/playback_rate_selection.js +++ b/ui/playback_rate_selection.js @@ -59,6 +59,15 @@ shaka.ui.PlaybackRateSelection = class extends shaka.ui.SettingsMenu { this.updatePlaybackRateSelection_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + shaka.ui.Utils.setDisplay(this.button, false); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + shaka.ui.Utils.setDisplay(this.button, true); + }); + } + // Set up all the strings in the user's preferred language. this.updateLocalizedStrings_(); this.addPlaybackRates_(); diff --git a/ui/recenter_vr.js b/ui/recenter_vr.js index b1959bf3d..eceefcd66 100644 --- a/ui/recenter_vr.js +++ b/ui/recenter_vr.js @@ -82,6 +82,15 @@ shaka.ui.RecenterVRButton = class extends shaka.ui.Element { this.checkAvailability_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.checkAvailability_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.checkAvailability_(); + }); + } + this.checkAvailability_(); } @@ -91,7 +100,7 @@ shaka.ui.RecenterVRButton = class extends shaka.ui.Element { */ checkAvailability_() { shaka.ui.Utils.setDisplay(this.recenterVRButton_, - this.controls.isPlayingVR()); + this.controls.isPlayingVR() && !this.isSubMenuOpened); } diff --git a/ui/remote_button.js b/ui/remote_button.js index ae3fe2fe6..b8338384a 100644 --- a/ui/remote_button.js +++ b/ui/remote_button.js @@ -119,6 +119,15 @@ shaka.ui.RemoteButton = class extends shaka.ui.Element { this.updateRemoteState_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.updateRemoteState_(/* force= */ true); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.updateRemoteState_(/* force= */ true); + }); + } + this.updateRemoteState_(/* force= */ true); this.updateIcon_(); } @@ -159,8 +168,9 @@ shaka.ui.RemoteButton = class extends shaka.ui.Element { canCast = false; } } - shaka.ui.Utils.setDisplay( - this.remoteButton_, canCast && availability && !disableRemote); + const display = canCast && availability && !disableRemote && + !this.isSubMenuOpened; + shaka.ui.Utils.setDisplay(this.remoteButton_, display); } else { shaka.ui.Utils.setDisplay(this.remoteButton_, false); } @@ -181,7 +191,7 @@ shaka.ui.RemoteButton = class extends shaka.ui.Element { handleAvailabilityChange(/* availability= */ true); } } else { - shaka.ui.Utils.setDisplay(this.remoteButton_, true); + shaka.ui.Utils.setDisplay(this.remoteButton_, !this.isSubMenuOpened); if (this.callbackId_ != -1) { // If remote device is connecting or connected, we should stop // watching remote device availability to save power. diff --git a/ui/resolution_selection.js b/ui/resolution_selection.js index dd20e9c74..a63236f56 100644 --- a/ui/resolution_selection.js +++ b/ui/resolution_selection.js @@ -114,6 +114,17 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { this.updateLabels_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.updateSelection_(); + this.updateLabels_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.updateSelection_(); + this.updateLabels_(); + }); + } + this.updateSelection_(); } @@ -273,7 +284,8 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { this.updateLocalizedStrings_(); - shaka.ui.Utils.setDisplay(this.button, numberOfTracks > 0); + shaka.ui.Utils.setDisplay( + this.button, numberOfTracks > 0 && !this.isSubMenuOpened); } /** diff --git a/ui/save_video_frame_button.js b/ui/save_video_frame_button.js index 95fa47984..66149336e 100644 --- a/ui/save_video_frame_button.js +++ b/ui/save_video_frame_button.js @@ -119,6 +119,15 @@ shaka.ui.SaveVideoFrameButton = class extends shaka.ui.Element { this.checkAvailability_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.checkAvailability_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.checkAvailability_(); + }); + } + this.checkAvailability_(); } @@ -127,7 +136,8 @@ shaka.ui.SaveVideoFrameButton = class extends shaka.ui.Element { * @private */ checkAvailability_() { - shaka.ui.Utils.setDisplay(this.button_, this.controls.canTakeScreenshot()); + shaka.ui.Utils.setDisplay(this.button_, + this.controls.canTakeScreenshot() && !this.isSubMenuOpened); } diff --git a/ui/settings_menu.js b/ui/settings_menu.js index 58f42da95..0472ba198 100644 --- a/ui/settings_menu.js +++ b/ui/settings_menu.js @@ -13,7 +13,6 @@ goog.require('shaka.ui.Icon'); goog.require('shaka.ui.Utils'); goog.require('shaka.util.Dom'); goog.require('shaka.util.FakeEvent'); -goog.require('shaka.util.Iterables'); goog.requireType('shaka.ui.Controls'); @@ -50,7 +49,10 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { /** @private {ResizeObserver} */ this.resizeObserver_ = null; - const resize = () => this.computeMaxHeight_(); + /** @private {MutationObserver} */ + this.mutationObserver_ = null; + + const resize = () => this.adjustCustomStyle_(); // Use ResizeObserver if available, fallback to window resize event if (window.ResizeObserver) { @@ -68,6 +70,10 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { this.resizeObserver_.disconnect(); this.resizeObserver_ = null; } + if (this.mutationObserver_) { + this.mutationObserver_.disconnect(); + this.mutationObserver_ = null; + } super.release(); } @@ -109,7 +115,11 @@ 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.classList.add('shaka-settings-menu'); + if (this.isSubMenu) { + this.menu.classList.add('shaka-sub-menu'); + } else { + this.menu.classList.add('shaka-settings-menu'); + } this.menu.classList.add('shaka-hidden'); /** @protected {!HTMLButtonElement}*/ @@ -128,8 +138,12 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { this.backSpan = shaka.util.Dom.createHTMLElement('span'); this.backButton.appendChild(this.backSpan); - const controlsContainer = this.controls.getControlsContainer(); - controlsContainer.appendChild(this.menu); + if (this.isSubMenu) { + this.parent.appendChild(this.menu); + } else { + const controlsContainer = this.controls.getControlsContainer(); + controlsContainer.appendChild(this.menu); + } } /** @private */ @@ -137,28 +151,38 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { // 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')) { + if (this.isSubMenu) { this.backIcon_.use(shaka.ui.Enums.MaterialDesignSVGIcons['BACK']); this.eventManager.listen(this.menu, 'click', () => { + this.controls.dispatchEvent(new shaka.util.FakeEvent('submenuclose')); shaka.ui.Utils.setDisplay(this.menu, false); shaka.ui.Utils.setDisplay(this.parent, true); + }); - const isDisplayed = - (element) => element.classList.contains('shaka-hidden') == false; + let prevHidden = this.parent.classList.contains('shaka-hidden'); - const Iterables = shaka.util.Iterables; - if (Iterables.some(this.parent.childNodes, isDisplayed)) { - // Focus on the first visible child of the overflow menu - const visibleElements = - Iterables.filter(this.parent.childNodes, isDisplayed); - /** @type {!HTMLElement} */ (visibleElements[0]).focus(); + this.mutationObserver_ = new MutationObserver((mutations) => { + if (!this.parent) { + return; } + for (const m of mutations) { + if (m.type === 'attributes' && m.attributeName === 'class') { + const newHidden = this.parent.classList.contains('shaka-hidden'); + if (newHidden && prevHidden != newHidden) { + this.controls.dispatchEvent( + new shaka.util.FakeEvent('submenuclose')); + shaka.ui.Utils.setDisplay(this.menu, false); + } + prevHidden = newHidden; + } + } + }); - // Make sure controls are displayed - this.controls.computeOpacity(); - - this.computeMaxHeight_(); + this.mutationObserver_.observe(this.parent, { + attributes: true, + attributeFilter: ['class'], + attributeOldValue: true, }); } } @@ -166,13 +190,19 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { /** @private */ onButtonClick_() { - 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); - this.computeMaxHeight_(); + if (!this.isSubMenu && this.controls.anySettingsMenusAreOpen()) { + this.controls.hideSettingsMenus(); } else { - shaka.ui.Utils.setDisplay(this.menu, false); + if (this.menu.classList.contains('shaka-hidden')) { + if (this.isSubMenu) { + this.controls.dispatchEvent(new shaka.util.FakeEvent('submenuopen')); + } + shaka.ui.Utils.setDisplay(this.menu, true); + shaka.ui.Utils.focusOnTheChosenItem(this.menu); + this.adjustCustomStyle_(); + } else { + shaka.ui.Utils.setDisplay(this.menu, false); + } } } @@ -180,7 +210,11 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { /** * @private */ - computeMaxHeight_() { + adjustCustomStyle_() { + if (this.isSubMenu) { + // Submenus take up the maximum size of the overflow element. + return; + } const rectMenu = this.menu.getBoundingClientRect(); const styleMenu = window.getComputedStyle(this.menu); const paddingTop = parseFloat(styleMenu.paddingTop); diff --git a/ui/statistics_button.js b/ui/statistics_button.js index 9f811c6da..e8b0c9300 100644 --- a/ui/statistics_button.js +++ b/ui/statistics_button.js @@ -192,6 +192,15 @@ shaka.ui.StatisticsButton = class extends shaka.ui.Element { this.onClick_(); this.updateLocalizedStrings_(); }); + + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + shaka.ui.Utils.setDisplay(this.button_, false); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + shaka.ui.Utils.setDisplay(this.button_, true); + }); + } } /** @private */ diff --git a/ui/text_selection.js b/ui/text_selection.js index 364204262..d88385b34 100644 --- a/ui/text_selection.js +++ b/ui/text_selection.js @@ -94,6 +94,15 @@ shaka.ui.TextSelection = class extends shaka.ui.SettingsMenu { this.updateTextLanguages_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.updateTextLanguages_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.updateTextLanguages_(); + }); + } + // Initialize caption state with a fake event. this.onCaptionStateChange_(); @@ -182,7 +191,8 @@ shaka.ui.TextSelection = class extends shaka.ui.SettingsMenu { this.controls.dispatchEvent( new shaka.util.FakeEvent('captionselectionupdated')); - shaka.ui.Utils.setDisplay(this.button, tracks.length > 0); + shaka.ui.Utils.setDisplay( + this.button, tracks.length > 0 && !this.isSubMenuOpened); } diff --git a/ui/toggle_stereoscopic.js b/ui/toggle_stereoscopic.js index 158986593..227235af8 100644 --- a/ui/toggle_stereoscopic.js +++ b/ui/toggle_stereoscopic.js @@ -84,6 +84,15 @@ shaka.ui.ToggleStereoscopicButton = class extends shaka.ui.Element { this.checkAvailability_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.checkAvailability_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.checkAvailability_(); + }); + } + this.checkAvailability_(); } @@ -93,7 +102,7 @@ shaka.ui.ToggleStereoscopicButton = class extends shaka.ui.Element { */ checkAvailability_() { shaka.ui.Utils.setDisplay(this.toggleStereoscopicButton_, - this.controls.isPlayingVR()); + this.controls.isPlayingVR() && !this.isSubMenuOpened); } diff --git a/ui/video_type_selection.js b/ui/video_type_selection.js index a4a1f7f69..d1166caff 100644 --- a/ui/video_type_selection.js +++ b/ui/video_type_selection.js @@ -76,6 +76,15 @@ shaka.ui.VideoTypeSelection = class extends shaka.ui.SettingsMenu { this.updateVideoRoles_(); }); + if (this.isSubMenu) { + this.eventManager.listen(this.controls, 'submenuopen', () => { + this.updateVideoRoles_(); + }); + this.eventManager.listen(this.controls, 'submenuclose', () => { + this.updateVideoRoles_(); + }); + } + // Set up all the strings in the user's preferred language. this.updateLocalizedStrings_(); this.updateVideoRoles_(); @@ -123,30 +132,28 @@ shaka.ui.VideoTypeSelection = class extends shaka.ui.SettingsMenu { } }; - if (roles.size <= 1) { - shaka.ui.Utils.setDisplay(this.button, false); - return; - } + if (roles.size > 1) { + for (const role of roles) { + const button = shaka.util.Dom.createButton(); + this.eventManager.listen(button, 'click', + () => this.onVideoRoleSelected_(role)); - for (const role of roles) { - const button = shaka.util.Dom.createButton(); - this.eventManager.listen(button, 'click', - () => this.onVideoRoleSelected_(role)); + const span = shaka.util.Dom.createHTMLElement('span'); + span.textContent = this.getRoleLabel_(role); + button.appendChild(span); - const span = shaka.util.Dom.createHTMLElement('span'); - span.textContent = this.getRoleLabel_(role); - button.appendChild(span); - - if (selectedTrack.roles.includes(role)) { - button.ariaSelected = 'true'; - button.appendChild(shaka.ui.Utils.checkmarkIcon()); - span.classList.add('shaka-chosen-item'); - this.currentSelection.textContent = span.textContent; + if (selectedTrack.roles.includes(role)) { + button.ariaSelected = 'true'; + button.appendChild(shaka.ui.Utils.checkmarkIcon()); + span.classList.add('shaka-chosen-item'); + this.currentSelection.textContent = span.textContent; + } + this.menu.appendChild(button); } - this.menu.appendChild(button); } - shaka.ui.Utils.setDisplay(this.button, true); + shaka.ui.Utils.setDisplay( + this.button, roles.size > 1 && !this.isSubMenuOpened); } /**