Skip to content

Commit

Permalink
feat(ui): Add Quality, Language, Playback, Captions buttons to contro…
Browse files Browse the repository at this point in the history
…l panel (shaka-project#3465)
  • Loading branch information
nbcl committed Jul 23, 2021
1 parent 4fee1de commit 481b378
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 43 deletions.
4 changes: 4 additions & 0 deletions docs/tutorials/ui-customization.md
Expand Up @@ -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).
<!-- TODO: If we add more buttons that can be put in the order this way, list them here. -->

Similarly, the 'overflowMenuButtons' configuration option can be used to control
Expand Down
74 changes: 74 additions & 0 deletions test/ui/ui_unit.js
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions ui/audio_language_selection.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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());
14 changes: 12 additions & 2 deletions ui/controls.js
Expand Up @@ -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_();
Expand Down Expand Up @@ -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_();
});
Expand Down
2 changes: 2 additions & 0 deletions ui/enums.js
Expand Up @@ -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',
Expand Down
10 changes: 0 additions & 10 deletions ui/less/overflow_menu.less
Expand Up @@ -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;
}
23 changes: 0 additions & 23 deletions ui/overflow_menu.js
Expand Up @@ -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_();
Expand Down
4 changes: 4 additions & 0 deletions ui/playback_rate_selection.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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());
4 changes: 4 additions & 0 deletions ui/resolution_selection.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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());
40 changes: 36 additions & 4 deletions ui/settings_menu.js
Expand Up @@ -33,6 +33,8 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element {

this.addMenu_();

this.inOverflowMenu_();

this.eventManager.listen(this.button, 'click', () => {
this.onButtonClick_();
});
Expand All @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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}*/
Expand All @@ -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);
}
}
};
12 changes: 8 additions & 4 deletions ui/text_selection.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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';
}

Expand Down Expand Up @@ -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());

0 comments on commit 481b378

Please sign in to comment.