- 1 :
/**
- 2 :
* @file menu-button.js
- 3 :
*/
- 4 :
import Button from '../button.js';
- 5 :
import Component from '../component.js';
- 6 :
import Menu from './menu.js';
- 7 :
import * as Dom from '../utils/dom.js';
- 8 :
import * as Events from '../utils/events.js';
- 9 :
import {toTitleCase} from '../utils/str.js';
- 10 :
import { IS_IOS } from '../utils/browser.js';
- 11 :
import document from 'global/document';
- 12 :
import keycode from 'keycode';
- 13 :
- 14 :
/**
- 15 :
* A `MenuButton` class for any popup {@link Menu}.
- 16 :
*
- 17 :
* @extends Component
- 18 :
*/
- 19 :
class MenuButton extends Component {
- 20 :
- 21 :
/**
- 22 :
* Creates an instance of this class.
- 23 :
*
- 24 :
* @param { import('../player').default } player
- 25 :
* The `Player` that this class should be attached to.
- 26 :
*
- 27 :
* @param {Object} [options={}]
- 28 :
* The key/value store of player options.
- 29 :
*/
- 30 :
constructor(player, options = {}) {
- 31 :
super(player, options);
- 32 :
- 33 :
this.menuButton_ = new Button(player, options);
- 34 :
- 35 :
this.menuButton_.controlText(this.controlText_);
- 36 :
this.menuButton_.el_.setAttribute('aria-haspopup', 'true');
- 37 :
- 38 :
// Add buildCSSClass values to the button, not the wrapper
- 39 :
const buttonClass = Button.prototype.buildCSSClass();
- 40 :
- 41 :
this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass;
- 42 :
this.menuButton_.removeClass('vjs-control');
- 43 :
- 44 :
this.addChild(this.menuButton_);
- 45 :
- 46 :
this.update();
- 47 :
- 48 :
this.enabled_ = true;
- 49 :
- 50 :
const handleClick = (e) => this.handleClick(e);
- 51 :
- 52 :
this.handleMenuKeyUp_ = (e) => this.handleMenuKeyUp(e);
- 53 :
- 54 :
this.on(this.menuButton_, 'tap', handleClick);
- 55 :
this.on(this.menuButton_, 'click', handleClick);
- 56 :
this.on(this.menuButton_, 'keydown', (e) => this.handleKeyDown(e));
- 57 :
this.on(this.menuButton_, 'mouseenter', () => {
- 58 :
this.addClass('vjs-hover');
- 59 :
this.menu.show();
- 60 :
Events.on(document, 'keyup', this.handleMenuKeyUp_);
- 61 :
});
- 62 :
this.on('mouseleave', (e) => this.handleMouseLeave(e));
- 63 :
this.on('keydown', (e) => this.handleSubmenuKeyDown(e));
- 64 :
}
- 65 :
- 66 :
/**
- 67 :
* Update the menu based on the current state of its items.
- 68 :
*/
- 69 :
update() {
- 70 :
const menu = this.createMenu();
- 71 :
- 72 :
if (this.menu) {
- 73 :
this.menu.dispose();
- 74 :
this.removeChild(this.menu);
- 75 :
}
- 76 :
- 77 :
this.menu = menu;
- 78 :
this.addChild(menu);
- 79 :
- 80 :
/**
- 81 :
* Track the state of the menu button
- 82 :
*
- 83 :
* @type {Boolean}
- 84 :
* @private
- 85 :
*/
- 86 :
this.buttonPressed_ = false;
- 87 :
this.menuButton_.el_.setAttribute('aria-expanded', 'false');
- 88 :
- 89 :
if (this.items && this.items.length <= this.hideThreshold_) {
- 90 :
this.hide();
- 91 :
this.menu.contentEl_.removeAttribute('role');
- 92 :
- 93 :
} else {
- 94 :
this.show();
- 95 :
this.menu.contentEl_.setAttribute('role', 'menu');
- 96 :
}
- 97 :
}
- 98 :
- 99 :
/**
- 100 :
* Create the menu and add all items to it.
- 101 :
*
- 102 :
* @return {Menu}
- 103 :
* The constructed menu
- 104 :
*/
- 105 :
createMenu() {
- 106 :
const menu = new Menu(this.player_, { menuButton: this });
- 107 :
- 108 :
/**
- 109 :
* Hide the menu if the number of items is less than or equal to this threshold. This defaults
- 110 :
* to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list
- 111 :
* it here because every time we run `createMenu` we need to reset the value.
- 112 :
*
- 113 :
* @protected
- 114 :
* @type {Number}
- 115 :
*/
- 116 :
this.hideThreshold_ = 0;
- 117 :
- 118 :
// Add a title list item to the top
- 119 :
if (this.options_.title) {
- 120 :
const titleEl = Dom.createEl('li', {
- 121 :
className: 'vjs-menu-title',
- 122 :
textContent: toTitleCase(this.options_.title),
- 123 :
tabIndex: -1
- 124 :
});
- 125 :
- 126 :
const titleComponent = new Component(this.player_, {el: titleEl});
- 127 :
- 128 :
menu.addItem(titleComponent);
- 129 :
}
- 130 :
- 131 :
this.items = this.createItems();
- 132 :
- 133 :
if (this.items) {
- 134 :
// Add menu items to the menu
- 135 :
for (let i = 0; i < this.items.length; i++) {
- 136 :
menu.addItem(this.items[i]);
- 137 :
}
- 138 :
}
- 139 :
- 140 :
return menu;
- 141 :
}
- 142 :
- 143 :
/**
- 144 :
* Create the list of menu items. Specific to each subclass.
- 145 :
*
- 146 :
* @abstract
- 147 :
*/
- 148 :
createItems() {}
- 149 :
- 150 :
/**
- 151 :
* Create the `MenuButtons`s DOM element.
- 152 :
*
- 153 :
* @return {Element}
- 154 :
* The element that gets created.
- 155 :
*/
- 156 :
createEl() {
- 157 :
return super.createEl('div', {
- 158 :
className: this.buildWrapperCSSClass()
- 159 :
}, {
- 160 :
});
- 161 :
}
- 162 :
- 163 :
/**
- 164 :
* Allow sub components to stack CSS class names for the wrapper element
- 165 :
*
- 166 :
* @return {string}
- 167 :
* The constructed wrapper DOM `className`
- 168 :
*/
- 169 :
buildWrapperCSSClass() {
- 170 :
let menuButtonClass = 'vjs-menu-button';
- 171 :
- 172 :
// If the inline option is passed, we want to use different styles altogether.
- 173 :
if (this.options_.inline === true) {
- 174 :
menuButtonClass += '-inline';
- 175 :
} else {
- 176 :
menuButtonClass += '-popup';
- 177 :
}
- 178 :
- 179 :
// TODO: Fix the CSS so that this isn't necessary
- 180 :
const buttonClass = Button.prototype.buildCSSClass();
- 181 :
- 182 :
return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`;
- 183 :
}
- 184 :
- 185 :
/**
- 186 :
* Builds the default DOM `className`.
- 187 :
*
- 188 :
* @return {string}
- 189 :
* The DOM `className` for this object.
- 190 :
*/
- 191 :
buildCSSClass() {
- 192 :
let menuButtonClass = 'vjs-menu-button';
- 193 :
- 194 :
// If the inline option is passed, we want to use different styles altogether.
- 195 :
if (this.options_.inline === true) {
- 196 :
menuButtonClass += '-inline';
- 197 :
} else {
- 198 :
menuButtonClass += '-popup';
- 199 :
}
- 200 :
- 201 :
return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
- 202 :
}
- 203 :
- 204 :
/**
- 205 :
* Get or set the localized control text that will be used for accessibility.
- 206 :
*
- 207 :
* > NOTE: This will come from the internal `menuButton_` element.
- 208 :
*
- 209 :
* @param {string} [text]
- 210 :
* Control text for element.
- 211 :
*
- 212 :
* @param {Element} [el=this.menuButton_.el()]
- 213 :
* Element to set the title on.
- 214 :
*
- 215 :
* @return {string}
- 216 :
* - The control text when getting
- 217 :
*/
- 218 :
controlText(text, el = this.menuButton_.el()) {
- 219 :
return this.menuButton_.controlText(text, el);
- 220 :
}
- 221 :
- 222 :
/**
- 223 :
* Dispose of the `menu-button` and all child components.
- 224 :
*/
- 225 :
dispose() {
- 226 :
this.handleMouseLeave();
- 227 :
super.dispose();
- 228 :
}
- 229 :
- 230 :
/**
- 231 :
* Handle a click on a `MenuButton`.
- 232 :
* See {@link ClickableComponent#handleClick} for instances where this is called.
- 233 :
*
- 234 :
* @param {Event} event
- 235 :
* The `keydown`, `tap`, or `click` event that caused this function to be
- 236 :
* called.
- 237 :
*
- 238 :
* @listens tap
- 239 :
* @listens click
- 240 :
*/
- 241 :
handleClick(event) {
- 242 :
if (this.buttonPressed_) {
- 243 :
this.unpressButton();
- 244 :
} else {
- 245 :
this.pressButton();
- 246 :
}
- 247 :
}
- 248 :
- 249 :
/**
- 250 :
* Handle `mouseleave` for `MenuButton`.
- 251 :
*
- 252 :
* @param {Event} event
- 253 :
* The `mouseleave` event that caused this function to be called.
- 254 :
*
- 255 :
* @listens mouseleave
- 256 :
*/
- 257 :
handleMouseLeave(event) {
- 258 :
this.removeClass('vjs-hover');
- 259 :
Events.off(document, 'keyup', this.handleMenuKeyUp_);
- 260 :
}
- 261 :
- 262 :
/**
- 263 :
* Set the focus to the actual button, not to this element
- 264 :
*/
- 265 :
focus() {
- 266 :
this.menuButton_.focus();
- 267 :
}
- 268 :
- 269 :
/**
- 270 :
* Remove the focus from the actual button, not this element
- 271 :
*/
- 272 :
blur() {
- 273 :
this.menuButton_.blur();
- 274 :
}
- 275 :
- 276 :
/**
- 277 :
* Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
- 278 :
* {@link ClickableComponent#handleKeyDown} for instances where this is called.
- 279 :
*
- 280 :
* @param {Event} event
- 281 :
* The `keydown` event that caused this function to be called.
- 282 :
*
- 283 :
* @listens keydown
- 284 :
*/
- 285 :
handleKeyDown(event) {
- 286 :
- 287 :
// Escape or Tab unpress the 'button'
- 288 :
if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- 289 :
if (this.buttonPressed_) {
- 290 :
this.unpressButton();
- 291 :
}
- 292 :
- 293 :
// Don't preventDefault for Tab key - we still want to lose focus
- 294 :
if (!keycode.isEventKey(event, 'Tab')) {
- 295 :
event.preventDefault();
- 296 :
// Set focus back to the menu button's button
- 297 :
this.menuButton_.focus();
- 298 :
}
- 299 :
// Up Arrow or Down Arrow also 'press' the button to open the menu
- 300 :
} else if (keycode.isEventKey(event, 'Up') || keycode.isEventKey(event, 'Down')) {
- 301 :
if (!this.buttonPressed_) {
- 302 :
event.preventDefault();
- 303 :
this.pressButton();
- 304 :
}
- 305 :
}
- 306 :
}
- 307 :
- 308 :
/**
- 309 :
* Handle a `keyup` event on a `MenuButton`. The listener for this is added in
- 310 :
* the constructor.
- 311 :
*
- 312 :
* @param {Event} event
- 313 :
* Key press event
- 314 :
*
- 315 :
* @listens keyup
- 316 :
*/
- 317 :
handleMenuKeyUp(event) {
- 318 :
// Escape hides popup menu
- 319 :
if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- 320 :
this.removeClass('vjs-hover');
- 321 :
}
- 322 :
}
- 323 :
- 324 :
/**
- 325 :
* This method name now delegates to `handleSubmenuKeyDown`. This means
- 326 :
* anyone calling `handleSubmenuKeyPress` will not see their method calls
- 327 :
* stop working.
- 328 :
*
- 329 :
* @param {Event} event
- 330 :
* The event that caused this function to be called.
- 331 :
*/
- 332 :
handleSubmenuKeyPress(event) {
- 333 :
this.handleSubmenuKeyDown(event);
- 334 :
}
- 335 :
- 336 :
/**
- 337 :
* Handle a `keydown` event on a sub-menu. The listener for this is added in
- 338 :
* the constructor.
- 339 :
*
- 340 :
* @param {Event} event
- 341 :
* Key press event
- 342 :
*
- 343 :
* @listens keydown
- 344 :
*/
- 345 :
handleSubmenuKeyDown(event) {
- 346 :
// Escape or Tab unpress the 'button'
- 347 :
if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- 348 :
if (this.buttonPressed_) {
- 349 :
this.unpressButton();
- 350 :
}
- 351 :
// Don't preventDefault for Tab key - we still want to lose focus
- 352 :
if (!keycode.isEventKey(event, 'Tab')) {
- 353 :
event.preventDefault();
- 354 :
// Set focus back to the menu button's button
- 355 :
this.menuButton_.focus();
- 356 :
}
- 357 :
} else {
- 358 :
// NOTE: This is a special case where we don't pass unhandled
- 359 :
// keydown events up to the Component handler, because it is
- 360 :
// just intending the keydown handling of the `MenuItem`
- 361 :
// in the `Menu` which already passes unused keys up.
- 362 :
}
- 363 :
}
- 364 :
- 365 :
/**
- 366 :
* Put the current `MenuButton` into a pressed state.
- 367 :
*/
- 368 :
pressButton() {
- 369 :
if (this.enabled_) {
- 370 :
this.buttonPressed_ = true;
- 371 :
this.menu.show();
- 372 :
this.menu.lockShowing();
- 373 :
this.menuButton_.el_.setAttribute('aria-expanded', 'true');
- 374 :
- 375 :
// set the focus into the submenu, except on iOS where it is resulting in
- 376 :
// undesired scrolling behavior when the player is in an iframe
- 377 :
if (IS_IOS && Dom.isInFrame()) {
- 378 :
// Return early so that the menu isn't focused
- 379 :
return;
- 380 :
}
- 381 :
- 382 :
this.menu.focus();
- 383 :
}
- 384 :
}
- 385 :
- 386 :
/**
- 387 :
* Take the current `MenuButton` out of a pressed state.
- 388 :
*/
- 389 :
unpressButton() {
- 390 :
if (this.enabled_) {
- 391 :
this.buttonPressed_ = false;
- 392 :
this.menu.unlockShowing();
- 393 :
this.menu.hide();
- 394 :
this.menuButton_.el_.setAttribute('aria-expanded', 'false');
- 395 :
}
- 396 :
}
- 397 :
- 398 :
/**
- 399 :
* Disable the `MenuButton`. Don't allow it to be clicked.
- 400 :
*/
- 401 :
disable() {
- 402 :
this.unpressButton();
- 403 :
- 404 :
this.enabled_ = false;
- 405 :
this.addClass('vjs-disabled');
- 406 :
- 407 :
this.menuButton_.disable();
- 408 :
}
- 409 :
- 410 :
/**
- 411 :
* Enable the `MenuButton`. Allow it to be clicked.
- 412 :
*/
- 413 :
enable() {
- 414 :
this.enabled_ = true;
- 415 :
this.removeClass('vjs-disabled');
- 416 :
- 417 :
this.menuButton_.enable();
- 418 :
}
- 419 :
}
- 420 :
- 421 :
Component.registerComponent('MenuButton', MenuButton);
- 422 :
export default MenuButton;