1
0
mirror of https://github.com/videojs/video.js.git synced 2025-01-04 06:48:49 +02:00

fix: remove event handlers when menu item is removed (#5748)

This commit is contained in:
Liu Ruen-shen 2019-01-26 03:26:36 +08:00 committed by Gary Katsevman
parent e8909231bd
commit 259ce71ee7
2 changed files with 182 additions and 13 deletions

View File

@ -36,6 +36,60 @@ class Menu extends Component {
this.focusedChild_ = -1;
this.on('keydown', this.handleKeyPress);
// All the menu item instances share the same blur handler provided by the menu container.
this.boundHandleBlur_ = Fn.bind(this, this.handleBlur);
this.boundHandleTapClick_ = Fn.bind(this, this.handleTapClick);
}
/**
* Add event listeners to the {@link MenuItem}.
*
* @param {Object} component
* The instance of the `MenuItem` to add listeners to.
*
*/
addEventListenerForItem(component) {
if (!(component instanceof Component)) {
return;
}
component.on('blur', this.boundHandleBlur_);
component.on(['tap', 'click'], this.boundHandleTapClick_);
}
/**
* Remove event listeners from the {@link MenuItem}.
*
* @param {Object} component
* The instance of the `MenuItem` to remove listeners.
*
*/
removeEventListenerForItem(component) {
if (!(component instanceof Component)) {
return;
}
component.off('blur', this.boundHandleBlur_);
component.off(['tap', 'click'], this.boundHandleTapClick_);
}
/**
* This method will be called indirectly when the component has been added
* before the component adds to the new menu instance by `addItem`.
* In this case, the original menu instance will remove the component
* by calling `removeChild`.
*
* @param {Object} component
* The instance of the `MenuItem`
*/
removeChild(component) {
if (typeof component === 'string') {
component = this.getChild(component);
}
this.removeEventListenerForItem(component);
super.removeChild(component);
}
/**
@ -46,20 +100,11 @@ class Menu extends Component {
*
*/
addItem(component) {
this.addChild(component);
component.on('blur', Fn.bind(this, this.handleBlur));
component.on(['tap', 'click'], Fn.bind(this, function(event) {
// Unpress the associated MenuButton, and move focus back to it
if (this.menuButton_) {
this.menuButton_.unpressButton();
const childComponent = this.addChild(component);
// don't focus menu button if item is a caption settings item
// because focus will move elsewhere
if (component.name() !== 'CaptionSettingsMenuItem') {
this.menuButton_.focus();
}
}
}));
if (childComponent) {
this.addEventListenerForItem(childComponent);
}
}
/**
@ -96,6 +141,8 @@ class Menu extends Component {
dispose() {
this.contentEl_ = null;
this.boundHandleBlur_ = null;
this.boundHandleTapClick_ = null;
super.dispose();
}
@ -123,6 +170,39 @@ class Menu extends Component {
}
}
/**
* Called when a `MenuItem` gets clicked or tapped.
*
* @param {EventTarget~Event} event
* The `click` or `tap` event that caused this function to be called.
*
* @listens click,tap
*/
handleTapClick(event) {
// Unpress the associated MenuButton, and move focus back to it
if (this.menuButton_) {
this.menuButton_.unpressButton();
const childComponents = this.children();
if (!Array.isArray(childComponents)) {
return;
}
const foundComponent = childComponents.filter(component => component.el() === event.target)[0];
if (!foundComponent) {
return;
}
// don't focus menu button if item is a caption settings item
// because focus will move elsewhere
if (foundComponent.name() !== 'CaptionSettingsMenuItem') {
this.menuButton_.focus();
}
}
}
/**
* Handle a `keydown` event on this menu. This listener is added in the constructor.
*

View File

@ -1,8 +1,12 @@
/* eslint-env qunit */
import * as DomData from '../../src/js/utils/dom-data';
import MenuButton from '../../src/js/menu/menu-button.js';
import Menu from '../../src/js/menu/menu.js';
import CaptionSettingsMenuItem from '../../src/js/control-bar/text-track-controls/caption-settings-menu-item';
import MenuItem from '../../src/js/menu/menu-item.js';
import TestHelpers from './test-helpers.js';
import * as Events from '../../src/js/utils/events.js';
import sinon from 'sinon';
QUnit.module('MenuButton');
@ -106,3 +110,88 @@ QUnit.test('should keep all the added menu items', function(assert) {
assert.ok(menuButton.el().contains(menuItem1.el()), 'the menu button contains the DOM element of `menuItem1` after second update');
assert.ok(menuButton.el().contains(menuItem2.el()), 'the menu button contains the DOM element of `menuItem2` after second update');
});
QUnit.test('should remove old event listeners when the menu item adds to the new menu', function(assert) {
const player = TestHelpers.makePlayer();
const menuButton = new MenuButton(player, {});
const oldMenu = new Menu(player, { menuButton });
const newMenu = new Menu(player, { menuButton });
oldMenu.addItem('MenuItem');
const menuItem = oldMenu.children()[0];
assert.ok(menuItem instanceof MenuItem, '`menuItem` should be the instanceof of `MenuItem`');
/**
* A reusable collection of assertions.
*/
function validateMenuEventListeners(watchedMenu) {
const eventData = DomData.getData(menuItem.eventBusEl_);
// `MenuButton`.`unpressButton` will be called when triggering click event on the menu item.
const unpressButtonSpy = sinon.spy(menuButton, 'unpressButton');
// `MenuButton`.`focus` will be called when triggering click event on the menu item.
const focusSpy = sinon.spy(menuButton, 'focus');
// `Menu`.`children` will be called when triggering blur event on the menu item.
const menuChildrenSpy = sinon.spy(watchedMenu, 'children');
// The number of blur listeners is two because `ClickableComponent`
// adds the blur event listener during the construction and
// `MenuItem` inherits from `ClickableComponent`.
assert.strictEqual(eventData.handlers.blur.length, 2, 'the number of blur listeners is two');
// Same reason mentioned above.
assert.strictEqual(eventData.handlers.click.length, 2, 'the number of click listeners is two');
const blurListenerAddedByMenu = eventData.handlers.blur[1];
const clickListenerAddedByMenu = eventData.handlers.click[1];
assert.strictEqual(
typeof blurListenerAddedByMenu.calledOnce,
'undefined',
'previous blur listener wrapped in the spy should be removed'
);
assert.strictEqual(
typeof clickListenerAddedByMenu.calledOnce,
'undefined',
'previous click listener wrapped in the spy should be removed'
);
const blurListenerSpy = eventData.handlers.blur[1] = sinon.spy(blurListenerAddedByMenu);
const clickListenerSpy = eventData.handlers.click[1] = sinon.spy(clickListenerAddedByMenu);
TestHelpers.triggerDomEvent(menuItem.el(), 'blur');
assert.ok(blurListenerSpy.calledOnce, 'blur event listener should be called');
assert.strictEqual(blurListenerSpy.getCall(0).args[0].target, menuItem.el(), 'event target should be the `menuItem`');
assert.ok(menuChildrenSpy.calledOnce, '`watchedMenu`.`children` has been called');
TestHelpers.triggerDomEvent(menuItem.el(), 'click');
assert.ok(clickListenerSpy.calledOnce, 'click event listener should be called');
assert.strictEqual(clickListenerSpy.getCall(0).args[0].target, menuItem.el(), 'event target should be the `menuItem`');
assert.ok(unpressButtonSpy.calledOnce, '`menuButton`.`unpressButtion` has been called');
assert.ok(focusSpy.calledOnce, '`menuButton`.`focus` has been called');
unpressButtonSpy.restore();
focusSpy.restore();
menuChildrenSpy.restore();
}
validateMenuEventListeners(oldMenu);
newMenu.addItem(menuItem);
validateMenuEventListeners(newMenu);
const focusSpy = sinon.spy(menuButton, 'focus');
const captionMenuItem = new CaptionSettingsMenuItem(player, {
kind: 'subtitles'
});
newMenu.addItem(captionMenuItem);
TestHelpers.triggerDomEvent(captionMenuItem.el(), 'click');
assert.ok(!focusSpy.called, '`menuButton`.`focus` should never be called');
focusSpy.restore();
});