mirror of
https://github.com/videojs/video.js.git
synced 2025-01-06 06:50:51 +02:00
fix: remove event handlers when menu item is removed (#5748)
This commit is contained in:
parent
e8909231bd
commit
259ce71ee7
@ -36,6 +36,60 @@ class Menu extends Component {
|
|||||||
this.focusedChild_ = -1;
|
this.focusedChild_ = -1;
|
||||||
|
|
||||||
this.on('keydown', this.handleKeyPress);
|
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) {
|
addItem(component) {
|
||||||
this.addChild(component);
|
const childComponent = 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();
|
|
||||||
|
|
||||||
// don't focus menu button if item is a caption settings item
|
if (childComponent) {
|
||||||
// because focus will move elsewhere
|
this.addEventListenerForItem(childComponent);
|
||||||
if (component.name() !== 'CaptionSettingsMenuItem') {
|
}
|
||||||
this.menuButton_.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,6 +141,8 @@ class Menu extends Component {
|
|||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this.contentEl_ = null;
|
this.contentEl_ = null;
|
||||||
|
this.boundHandleBlur_ = null;
|
||||||
|
this.boundHandleTapClick_ = null;
|
||||||
|
|
||||||
super.dispose();
|
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.
|
* Handle a `keydown` event on this menu. This listener is added in the constructor.
|
||||||
*
|
*
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
/* eslint-env qunit */
|
/* eslint-env qunit */
|
||||||
|
import * as DomData from '../../src/js/utils/dom-data';
|
||||||
import MenuButton from '../../src/js/menu/menu-button.js';
|
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 MenuItem from '../../src/js/menu/menu-item.js';
|
||||||
import TestHelpers from './test-helpers.js';
|
import TestHelpers from './test-helpers.js';
|
||||||
import * as Events from '../../src/js/utils/events.js';
|
import * as Events from '../../src/js/utils/events.js';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
QUnit.module('MenuButton');
|
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(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');
|
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();
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user