Skip to content

Commit daf40bd

Browse files
fix(spatial-navigation): focus lost in error modal (#8817)
## Description The spatial-navigation is unable to focus certain elements of the error modal when this appears, this PR will fix that ## Specific Changes proposed Allow the spatial-navigation to focus certain non-component elements in the error modal ## Requirements Checklist - [x] Feature implemented / Bug fixed - [ ] If necessary, more likely in a feature request than a bug fix - [ ] Change has been verified in an actual browser (Chrome, Firefox, IE) - [ ] Unit Tests updated or fixed - [ ] Docs/guides updated - [ ] Example created ([starter template on JSBin](https://codepen.io/gkatsev/pen/GwZegv?editors=1000#0)) - [ ] Has no DOM changes which impact accessiblilty or trigger warnings (e.g. Chrome issues tab) - [ ] Has no changes to JSDoc which cause `npm run docs:api` to error - [ ] Reviewed by Two Core Contributors
1 parent 76e99b7 commit daf40bd

File tree

3 files changed

+183
-4
lines changed

3 files changed

+183
-4
lines changed

src/js/modal-dialog.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,14 @@ class ModalDialog extends Component {
374374
if (closeButton) {
375375
parentEl.appendChild(closeButton.el_);
376376
}
377+
378+
/**
379+
* Fired after `ModalDialog` is re-filled with content & close button is appended.
380+
*
381+
* @event ModalDialog#aftermodalfill
382+
* @type {Event}
383+
*/
384+
this.trigger('aftermodalfill');
377385
}
378386

379387
/**

src/js/spatial-navigation.js

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,17 @@ class SpatialNavigation extends EventTarget {
5656
this.player_.on('modalclose', () => {
5757
this.refocusComponent();
5858
});
59-
this.player_.on('error', () => {
60-
this.focus(this.updateFocusableComponents()[0]);
61-
});
6259
this.player_.on('focusin', this.handlePlayerFocus_.bind(this));
6360
this.player_.on('focusout', this.handlePlayerBlur_.bind(this));
6461
this.isListening_ = true;
62+
this.player_.errorDisplay.on('aftermodalfill', () => {
63+
this.updateFocusableComponents();
64+
65+
// Focus the buttons of the modal
66+
if (this.focusableComponents.length > 1) {
67+
this.focusableComponents[1].focus();
68+
}
69+
});
6570
}
6671

6772
/**
@@ -270,9 +275,65 @@ class SpatialNavigation extends EventTarget {
270275
focusableComponents.push(value);
271276
}
272277
}
278+
279+
// TODO - Refactor the following logic after refactor of videojs-errors elements to be components is done.
280+
if (value.name_ === 'ErrorDisplay' && value.opened_) {
281+
const buttonContainer = value.el_.querySelector('.vjs-errors-ok-button-container');
282+
283+
if (buttonContainer) {
284+
const modalButtons = buttonContainer.querySelectorAll('button');
285+
286+
modalButtons.forEach((element, index) => {
287+
// Add elements as objects to be handled by the spatial navigation
288+
focusableComponents.push({
289+
name: () => {
290+
return 'ModalButton' + (index + 1);
291+
},
292+
el: () => element,
293+
getPositions: () => {
294+
const rect = element.getBoundingClientRect();
295+
296+
// Creating objects that mirror DOMRectReadOnly for boundingClientRect and center
297+
const boundingClientRect = {
298+
x: rect.x,
299+
y: rect.y,
300+
width: rect.width,
301+
height: rect.height,
302+
top: rect.top,
303+
right: rect.right,
304+
bottom: rect.bottom,
305+
left: rect.left
306+
};
307+
308+
// Calculating the center position
309+
const center = {
310+
x: rect.left + rect.width / 2,
311+
y: rect.top + rect.height / 2,
312+
width: 0,
313+
height: 0,
314+
top: rect.top + rect.height / 2,
315+
right: rect.left + rect.width / 2,
316+
bottom: rect.top + rect.height / 2,
317+
left: rect.left + rect.width / 2
318+
};
319+
320+
return {
321+
boundingClientRect,
322+
center
323+
};
324+
},
325+
// Asume that the following are always focusable
326+
getIsAvailableToBeFocused: () => true,
327+
getIsFocusable: (el) => true,
328+
focus: () => element.focus()
329+
});
330+
});
331+
}
332+
}
273333
});
274334

275335
this.focusableComponents = focusableComponents;
336+
276337
return this.focusableComponents;
277338
}
278339

test/unit/spatial-navigation.test.js

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import TestHelpers from './test-helpers.js';
55
import sinon from 'sinon';
66
import document from 'global/document';
77
import TextTrackSelect from '../../src/js/tracks/text-track-select';
8+
import * as Dom from '../../src/js/utils/dom.js';
89

910
QUnit.module('SpatialNavigation', {
1011
beforeEach() {
@@ -44,7 +45,6 @@ QUnit.test('start method initializes event listeners', function(assert) {
4445
assert.ok(onSpy.calledWith('loadedmetadata'), 'loadedmetadata event listener added');
4546
assert.ok(onSpy.calledWith('modalKeydown'), 'modalKeydown event listener added');
4647
assert.ok(onSpy.calledWith('modalclose'), 'modalclose event listener added');
47-
assert.ok(onSpy.calledWith('error'), 'error event listener added');
4848

4949
// Additionally, check if isListening_ flag is set
5050
assert.ok(this.spatialNav.isListening_, 'isListening_ flag is set');
@@ -502,3 +502,113 @@ QUnit.test('error on player calls updateFocusableComponents', function(assert) {
502502

503503
assert.ok(updateFocusableComponentsSpy.calledOnce, 'on error event spatial navigation should call "updateFocusableComponents"');
504504
});
505+
506+
QUnit.test('error on player focus the second focusable element of error modal', function(assert) {
507+
this.spatialNav.start();
508+
509+
const firstComponent = {
510+
name: () => 'firstComponent',
511+
el: () => document.createElement('div'),
512+
focus: sinon.spy(),
513+
getIsAvailableToBeFocused: () => true
514+
};
515+
516+
const secondComponent = {
517+
name: () => 'secondComponent',
518+
el: () => document.createElement('div'),
519+
focus: sinon.spy(),
520+
getIsAvailableToBeFocused: () => true
521+
};
522+
523+
this.spatialNav.focusableComponents = [firstComponent, secondComponent];
524+
this.spatialNav.getCurrentComponent = () => firstComponent;
525+
this.spatialNav.updateFocusableComponents = () => [firstComponent, secondComponent];
526+
527+
this.player.error({
528+
code: 1,
529+
dismiss: true
530+
});
531+
532+
assert.ok(secondComponent.focus.calledOnce, 'Focus should move to the second component');
533+
assert.notOk(firstComponent.focus.called, 'Focus should not remain on the first component');
534+
});
535+
536+
QUnit.test('on error, modalButtons should get the buttons if those are available', function(assert) {
537+
this.spatialNav.start();
538+
539+
const buttonContainer = Dom.createEl('div', {}, {class: 'vjs-errors-ok-button-container'});
540+
541+
const testEl1 = Dom.createEl('button', {}, {class: 'c1'});
542+
543+
// Add first element to error modal
544+
buttonContainer.appendChild(testEl1);
545+
546+
const testEl2 = Dom.createEl('button', {}, {class: 'c1'});
547+
548+
// Add second element to error modal
549+
buttonContainer.appendChild(testEl2);
550+
this.player.errorDisplay.el().appendChild(buttonContainer);
551+
552+
this.player.error({
553+
code: 1,
554+
dismiss: true
555+
});
556+
557+
this.spatialNav.getCurrentComponent = () => this.spatialNav.focusableComponents[0];
558+
559+
const getPositionsEl1Spy = sinon.spy(this.spatialNav.focusableComponents[0], 'getPositions');
560+
const getPositionsEl2Spy = sinon.spy(this.spatialNav.focusableComponents[1], 'getPositions');
561+
562+
this.spatialNav.move('left');
563+
564+
assert.strictEqual(this.spatialNav.focusableComponents.length, 2, 'button elements are now part of the array of focusableComponents');
565+
assert.ok(getPositionsEl1Spy.calledOnce, 'getPositions method called on button');
566+
assert.ok(getPositionsEl2Spy.calledOnce, 'getPositions method called on button');
567+
assert.strictEqual(this.spatialNav.focusableComponents[0].name(), 'ModalButton1', 'testEl1 name should be ModalButton1');
568+
assert.strictEqual(this.spatialNav.focusableComponents[1].name(), 'ModalButton2', 'testEl2 name should be ModalButton2');
569+
570+
getPositionsEl1Spy.restore();
571+
getPositionsEl2Spy.restore();
572+
});
573+
574+
QUnit.test('on error, modalButtons added functions should work properly', function(assert) {
575+
this.spatialNav.start();
576+
577+
const buttonContainer = Dom.createEl('div', {}, {class: 'vjs-errors-ok-button-container'});
578+
579+
const testEl1 = Dom.createEl('button', {}, {class: 'c1'});
580+
581+
// Add first element to error modal
582+
buttonContainer.appendChild(testEl1);
583+
584+
this.player.errorDisplay.el().appendChild(buttonContainer);
585+
586+
this.player.error({ code: 1, dismiss: true });
587+
588+
assert.strictEqual(this.spatialNav.focusableComponents[0].el() instanceof Element, true, 'el function from modal buttons should return a DOM element'); // eslint-disable-line no-undef
589+
assert.strictEqual(this.spatialNav.focusableComponents[0].getIsFocusable(), true, 'getIsFocusable function from modal buttons is always true');
590+
assert.strictEqual(this.spatialNav.focusableComponents[0].getIsAvailableToBeFocused(), true, 'getIsAvailableToBeFocused function from modal buttons is always true');
591+
assert.strictEqual(this.spatialNav.focusableComponents[0].getIsAvailableToBeFocused(), true, 'getIsAvailableToBeFocused function from modal buttons is always true');
592+
assert.strictEqual(typeof this.spatialNav.focusableComponents[0].getPositions(), 'object', 'focusableComponents function from modal buttons should return an object');
593+
});
594+
595+
QUnit.test('If component passes the required functions it should be added to focusableComponents', function(assert) {
596+
this.spatialNav.start();
597+
598+
const firstComponent = {
599+
name_: 'firstComponent',
600+
name: () => this.name,
601+
el_: document.createElement('div'),
602+
el: () => this.el_,
603+
focus: sinon.spy(),
604+
getIsAvailableToBeFocused: () => true,
605+
getIsFocusable: () => true
606+
};
607+
608+
this.player.children_.push(firstComponent);
609+
this.spatialNav.getCurrentComponent = () => firstComponent;
610+
this.spatialNav.updateFocusableComponents();
611+
612+
assert.strictEqual(this.spatialNav.focusableComponents.length, 1, 'focusableComponents array should have 1 component');
613+
assert.strictEqual(this.spatialNav.focusableComponents[0].name_, 'firstComponent', 'the name of the component in focusableComponents array should be "firstComponent"');
614+
});

0 commit comments

Comments
 (0)