Skip to content

Commit 21b4a52

Browse files
bzizmoCarlosVillasenordzianis-dashkevichwseymour15Carlos Villasenor Castillo
authored
feat: implement spatial navigation (#8570)
* feat(player): add spatialNavigation feature Adds spatialNavigation feature to enhance user experience - Implemented spatial navigation in slider component - Enhanced player functionality for improved navigation * feat(player): add spatialNavigation class Adds spatialNavigation class to manage spatial-navigation-polyfill - Set class SpatialNavigation on its own file - Imported SpatialNavigation class on component class * feat(player): update spatialNavigation class Adds 3 methods to spatialNavigation class to manage spatial-navigation-polyfill - Added start() to: Start listen of keydown events - Added stop() to: Stop listen key down events - Added getComponents() to: Get current focusable components * feat(player): modify spatialNavigation class & modify component class Modify spatialNavigation class: -Remove unrequired version of function ‘getComponents’ Modify component class: -Add function ‘getIsFocusable’ * Added methods getPositions, handleFocus and handleBLur for spatial navigation needs * feat(player): modify Component class, BigPlayButton class & ClickableComponent class Modify Component class: -Add method getIsAvailableToBeFocused -Modify method getIsFocusable to only focus on finding focusable candidates Modify spatialNavigation class: -Remove unrequired method ‘getIsFocusable’ Modify component class: -Remove unrequired method ‘getIsFocusable’ * Added import in player.js, Created base methods inside spatial-navigation.js * feat(player): modify Component class & SpatialNavigation class Modify Component class: -Modify method getIsAvailableToBeFocused to be more strict on candidates Modify spatialNavigation class: -Modify method getComponents to get all focusable components * feat(player): modify Component class Modify Component class: -Add documentation to ‘isVisible’ function * added keydown event logic for spatial-navigation * feat(player): modify SpatialNavigation class Modify SpatialNavigation class: -Modify documentation of functions * feat(player): modify SpatialNavigation class Modify SpatialNavigation class: -Add ‘clear’ & ‘remove’ methods * feat(player): modify SpatialNavigation class Modify SpatialNavigation class: -Add documentation of functions * feat(player): modify SpatialNavigation class Modify SpatialNavigation class: -Add function ‘getCurretComponent’‘’ * feat(player): modify SpatialNavigation class Modify SpatialNavigation class: -Add documentation for ‘findBestCandidate’ method * Added logic for moving focus to the best candidate * Implemented move, findBestCandidate, isInDirection, and calculateDistance methods for spatial navigation logic * Added a new player option enableKeydownListener, Added gap: 1px to control-bar for spatial-navigation-polyfill needs * feat(player): modify SpatialNavigation class & Component class Modify SpatialNavigation class: -Add function ‘handlePlayerBlur’ -Add function ‘handlePlayerFocus’ Modify Component class: -Modify ‘handleBlur’ -Modify ‘handleFocus’ * Removed enableKeydownListener flag, as user should start the SpatialNavigation manually * Added functionality to track changes in the focusableComponents list (custom event focusableComponentsChanged) * feat(player): modify SpatialNavigation class, ModalDialog & Component class Modify SpatialNavigation class: -Add ‘lastFocusedComponent’ -Add function ‘refocusComponent’ Modify ModalDialog class: -Add condition on ‘close’ function Modify Component class: -Modify ‘handleBlur’ to store blurred component * feat(player): modify ModalDialog Modify ModalDialog: -Add condition to close Modal on Backspace * Refactor SpatialNavigation to use player.spatialNavigation * Added a new custom event endOfFocusableComponents * Added new styles for focused elements in case spatial navigation is enabled * feat(player): modify SpatialNavigation class: -Add condition so getComponents can get as candidates the UI elements from the playlist-ui * Changed to window.SpatialNabigation to this.player_.spatialNavigation * feat(player): modify text-track-settings, created test-track-settings-colors.js, text-track-settings-font.js,text-track-fieldset.js & text-track-select.js: Modify text-track-settings class: - Add changes so newly created components can work as content of the modal. - Create new components as a refactor of the contents of text-track-settings * changed handleKeyDown inside component.js, getComponents method is now iterating player.children * feat(player): create TrackSettingsControls Component & Modify TextTrackSettings Create TrackSettingsControls Component: -Create Component to show buttons reset & done as components. Modify TextTrackSettings: -Add Component TrackSettingsControls in TextTrackSettings * feat(player): Modify ModalDialog Modify ModalDialog: -Add condition for stop propagation of event inside of ModalDialog when spatialNavigation is enabled * getIsFocusable and getIsAvailableToBeFocused methods are now accepting el as a parameter, added a new methods findSuitableDOMChild and focus for spatialNavigation class * feat(player): Modify TextTrackSettings: Modify TextTrackSettings: -Remove unrequired methods to create DOM elements since now those are created by Components. * feat(player): Modify CaptionSettingsMenuItem: Modify CaptionSettingsMenuItem: -Add condition to focus component of TextTrackSelect when modal is open * feat(player): Modify TextTrackSelect & TextTrackFieldset: Modify TextTrackSelect : Modify TextTrackFieldset: -Add comments to certain functions to explain the code * feat(player): Modify TrackSettingsControls: Modify TrackSettingsControls: -Remove unrequired comments & add comments to certain functions to explain the code * feat(player): Modify SpatialNavigation, Component & ModalDialog: Modify SpatialNavigation: Modify Component: Modify ModalDialog: -Add & update comments of documentation. * Handle ENTER keydown in Modals when spatial navigation is enabled * feat(player): Modify ModalDialog, spatialNavigation, TrackSettingsControls, TextTrackFieldset, TextTrackSelect, TrackSettingsColors, TrackSettingsFont: Modify ModalDialog: Modify spatialNavigation: Modify TrackSettingsControls: Modify TextTrackFieldset: Modify TextTrackSelect: Modify TrackSettingsColors: Modify TrackSettingsFont: -Add & update comments of documentation. * Implement additional RCU controls * feat(player): Modify Component class: Modify Component : -Remove unrequired condition inside of handleFocus method. * feat(player): Modify ModalDialog & CaptionSettingsMenuItem Modify ModalDialog: Modify CaptionSettingsMenuItem: -Modify spatialNavigation condition to be more specific regarding spatialNavigation implementation. * feat(player): Modify SpatialNavigation class: Modify SpatialNavigation : -Fix bug where ‘enter’ press was not working properly on select component inside of the ‘vjs-text-track-settings’ modal. * feat(player): Modify SpatialNavigation class: Modify SpatialNavigation : -Minor improvements on the loops of certain functions to stop when they have found the element they are looking for. -Implement minor spacing formatting on switch statement. * Update src/js/component.js More understandable documentation. Co-authored-by: Dzianis Dashkevich <98566601+dzianis-dashkevich@users.noreply.github.com> * Update src/js/component.js More understandable documentation. Co-authored-by: Dzianis Dashkevich <98566601+dzianis-dashkevich@users.noreply.github.com> * feat(player): Modify SpatialNavigation & Component class: Modify Component class : Modify SpatialNavigation class : -Modify ‘getIsFocusable’ function to use ‘this.el_’ instead of ‘el’ parameter * feat(player): Modify SpatialNavigation class: Modify SpatialNavigation class : -Refactor onKeyDown function to use static data & return when pause is true. * feat(player): Modify SpatialNavigation class: Modify SpatialNavigation class : -Refactor to use ‘.el()’ instead of ‘.el_’ * Update src/js/spatial-navigation.js Co-authored-by: Walter Seymour <walterseymour15@gmail.com> * feat(player): Modify ModalDialog class & MenuItem class: Modify ModalDialog class : Modify MenuItem class : -Correct typo of ‘isSpatialNavlistening’ to ‘isSpatialNavListening’. * removed unused property, remove this.focus, which was added for testing purposes * Changed parameters to private, removed redundant code, removed initialFocusedComponent parameter, change STEP_SECONDS to static * feat(player): solve remaining conflict: Modify Spatial Navigation class : - Solve conflict * feat(player): Rename TrackSettingsColors & TrackSettingsFont * feat(player): Remove unrequired functions calls from components TextTrackSettingsColors & TextTrackSettingsFont. * feat(player): Update spatial-navigation.js's keypress return keyword. * bind focus and blur just if spatial navigation is enabled, add 1px gap if spatial navigation is enabled * feat(player): Modify calls on 'isListening' & 'isPaused' for ModalDialog & TextTrackMenuItem * feat(player): remove unrequired object on component 'TrackSettingsControls' * Removed 1px gap * feat(player): Rename function ‘getComponents’ to ‘updateFocusableComponents’ * Changed SpatialNavigation class to extend EventTarget, removed redundant methods for events * fix(player): fix call of 'getIsAvailableToBeFocused' that was throwing an error. * removed Static maps for key presses and extended keycode with the missing keys * refactor(player): Modify functions of 'getIsDisabled', 'getIsExpresslyInert' & 'getIsFocusable' to be more in pair when stablished code of the player. * Conditional assignment for keycode.codes.back based on platform, changed Backspace to Back key for Modal closing * Extend the object for reverse lookup, prenet Up/down keys to open a menu if spatial navigation is anabled * refactor(player): Refactor 'SpatialNavKeycodes' file to not patch 'keycode' dependency * fix(pllayer): fix issue related to 'back' not being used properly in function 'isEventKey' * feat(player): Rename imports of 'spatial-navigation-keycode' to have their extension * feat(player): Add example of use of 'Client app uses a global spatial-navigation solution' * feat(player): rename 'spatial-navigation-keycode.js' filename * Fix on src chnage issue, ESC button closing modal, expand vjs-modal-dialog * change file name and object name * fix: Update ids of labels to use 'guid' so unit test works properly * fix: update localized text in text-track-settings-font & text-track-settings * Mark some methods as private * fix: modify content of modal 'text-track-settings' to change language properly * fix: add missing '.' in jsdoc of text-track components * feature: add unit test for 'text-track-select' component * Add test for Spatial Navigation * test(player): Add minor test related to 'handleBlur' & 'handleFocus' * feat(player): Remove unrequired files from 'react-video-nav-app' * test(player): Add small test to check if 'getPositions' returns required properties * test(player): add test to verify 'getPositions()' properties are not empty * Add missing tests for performMediaAction_ and move * test(player): add test to for 'component.js' related to 'handleBlur' * test(player): add minor test in component related to test keypress propagation event * test(player): add test for component related to 'getIsAvailableToBeFocused' function * test(player): add test for Modal Dialog related to call function of spatial navigation * test(player): add tests for 'spatial-navigation-key-codes' * test(player): add tests for keycodes related to 'should return event name if keyCode is not available' * test(player): add minor test for case when not required parametters are passed * test(player): add test for 'caption-settings-menu-item' * feat(player): remove 'react-video-nav-app' * Move handleFocus and handleBlur from components.js to spatial-navigation.js * refactor(player): refactor 'searchForTrackSelect' to be handled in the spatial navigation * remove unrequired code in function 'searchForTrackSelect' * update documentation comment to be in pair to its current use * remove spatial navigation keydown from modal dialog and move it to spatial navigation class, modify the modal-dialog test accordingly * remove useless tests * Remove caption-settings-menu-item.test.js * Add minor test to 'searchForTrackSelect' in spatial-navigation.test.js * Add unit test for back key and listening to events --------- Co-authored-by: CarlosVillasenor <carlosdeveloper9@gmail.com> Co-authored-by: Dzianis Dashkevich <98566601+dzianis-dashkevich@users.noreply.github.com> Co-authored-by: Walter Seymour <walterseymour15@gmail.com> Co-authored-by: Carlos Villasenor Castillo <cvillasenor@Carloss-MacBook-Pro.local>
1 parent 582c35f commit 21b4a52

21 files changed

+2125
-235
lines changed

src/css/components/_button.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
appearance: none;
1616
}
1717

18+
// Replacement for focus in case spatial navigation is enabled
19+
.video-js.vjs-spatial-navigation-enabled .vjs-button:focus {
20+
outline: 0.0625em solid rgba($primary-foreground-color, 1);
21+
box-shadow: none;
22+
}
23+
1824
.vjs-control .vjs-button {
1925
width: 100%;
2026
height: 100%;

src/css/components/_captions-settings.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
background-color: rgba($primary-background-color, 0.75);
44
color: $primary-foreground-color;
55
height: 70%;
6+
7+
// When Spatial Navigation is enabled
8+
.vjs-spatial-navigation-enabled & {
9+
height: 80%;
10+
}
611
}
712

813
// Hide if an error occurs

src/css/components/_control-bar.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
@include background-color-with-alpha($primary-background-color, $primary-background-transparency);
1111
}
1212

13+
.video-js.vjs-spatial-navigation-enabled .vjs-control-bar {
14+
gap: 1px;
15+
}
16+
1317
// Locks the display only if:
1418
// - controls are not disabled
1519
// - native controls are not used

src/css/components/_slider.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@
1818

1919
@include box-shadow(0 0 1em $primary-foreground-color);
2020
}
21+
22+
// Replacement for focus in case spatial navigation is enabled
23+
.video-js.vjs-spatial-navigation-enabled .vjs-slider:focus {
24+
outline: 0.0625em solid rgba($primary-foreground-color, 1);
25+
}

src/js/component.js

Lines changed: 193 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,49 @@ class Component {
12831283
return this.currentDimension('height');
12841284
}
12851285

1286+
/**
1287+
* Retrieves the position and size information of the component's element.
1288+
*
1289+
* @return {Object} An object with `boundingClientRect` and `center` properties.
1290+
* - `boundingClientRect`: An object with properties `x`, `y`, `width`,
1291+
* `height`, `top`, `right`, `bottom`, and `left`, representing
1292+
* the bounding rectangle of the element.
1293+
* - `center`: An object with properties `x` and `y`, representing
1294+
* the center point of the element. `width` and `height` are set to 0.
1295+
*/
1296+
getPositions() {
1297+
const rect = this.el_.getBoundingClientRect();
1298+
1299+
// Creating objects that mirror DOMRectReadOnly for boundingClientRect and center
1300+
const boundingClientRect = {
1301+
x: rect.x,
1302+
y: rect.y,
1303+
width: rect.width,
1304+
height: rect.height,
1305+
top: rect.top,
1306+
right: rect.right,
1307+
bottom: rect.bottom,
1308+
left: rect.left
1309+
};
1310+
1311+
// Calculating the center position
1312+
const center = {
1313+
x: rect.left + rect.width / 2,
1314+
y: rect.top + rect.height / 2,
1315+
width: 0,
1316+
height: 0,
1317+
top: rect.top + rect.height / 2,
1318+
right: rect.left + rect.width / 2,
1319+
bottom: rect.top + rect.height / 2,
1320+
left: rect.left + rect.width / 2
1321+
};
1322+
1323+
return {
1324+
boundingClientRect,
1325+
center
1326+
};
1327+
}
1328+
12861329
/**
12871330
* Set the focus to this component
12881331
*/
@@ -1308,8 +1351,8 @@ class Component {
13081351
if (this.player_) {
13091352

13101353
// We only stop propagation here because we want unhandled events to fall
1311-
// back to the browser. Exclude Tab for focus trapping.
1312-
if (!keycode.isEventKey(event, 'Tab')) {
1354+
// back to the browser. Exclude Tab for focus trapping, exclude also when spatialNavigation is enabled.
1355+
if (!keycode.isEventKey(event, 'Tab') && !(this.player_.options_.playerOptions.spatialNavigation && this.player_.options_.playerOptions.spatialNavigation.enabled)) {
13131356
event.stopPropagation();
13141357
}
13151358
this.player_.handleKeyDown(event);
@@ -1765,6 +1808,154 @@ class Component {
17651808
});
17661809
}
17671810

1811+
/**
1812+
* Decide whether an element is actually disabled or not.
1813+
*
1814+
* @function isActuallyDisabled
1815+
* @param element {Node}
1816+
* @return {boolean}
1817+
*
1818+
* @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled}
1819+
*/
1820+
getIsDisabled() {
1821+
return Boolean(this.el_.disabled);
1822+
}
1823+
1824+
/**
1825+
* Decide whether the element is expressly inert or not.
1826+
*
1827+
* @see {@link https://html.spec.whatwg.org/multipage/interaction.html#expressly-inert}
1828+
* @function isExpresslyInert
1829+
* @param element {Node}
1830+
* @return {boolean}
1831+
*/
1832+
getIsExpresslyInert() {
1833+
return this.el_.inert && !this.el_.ownerDocument.documentElement.inert;
1834+
}
1835+
1836+
/**
1837+
* Determine whether or not this component can be considered as focusable component.
1838+
*
1839+
* @param {HTMLElement} el - The HTML element representing the component.
1840+
* @return {boolean}
1841+
* If the component can be focused, will be `true`. Otherwise, `false`.
1842+
*/
1843+
getIsFocusable() {
1844+
return this.el_.tabIndex >= 0 && !(this.getIsDisabled() || this.getIsExpresslyInert());
1845+
}
1846+
1847+
/**
1848+
* Determine whether or not this component is currently visible/enabled/etc...
1849+
*
1850+
* @param {HTMLElement} el - The HTML element representing the component.
1851+
* @return {boolean}
1852+
* If the component can is currently visible & enabled, will be `true`. Otherwise, `false`.
1853+
*/
1854+
getIsAvailableToBeFocused(el) {
1855+
/**
1856+
* Decide the style property of this element is specified whether it's visible or not.
1857+
*
1858+
* @function isVisibleStyleProperty
1859+
* @param element {CSSStyleDeclaration}
1860+
* @return {boolean}
1861+
*/
1862+
function isVisibleStyleProperty(element) {
1863+
const elementStyle = window.getComputedStyle(element, null);
1864+
const thisVisibility = elementStyle.getPropertyValue('visibility');
1865+
const thisDisplay = elementStyle.getPropertyValue('display');
1866+
const invisibleStyle = ['hidden', 'collapse'];
1867+
1868+
return (thisDisplay !== 'none' && !invisibleStyle.includes(thisVisibility));
1869+
}
1870+
1871+
/**
1872+
* Decide whether the element is being rendered or not.
1873+
* 1. If an element has the style as "visibility: hidden | collapse" or "display: none", it is not being rendered.
1874+
* 2. If an element has the style as "opacity: 0", it is not being rendered.(that is, invisible).
1875+
* 3. If width and height of an element are explicitly set to 0, it is not being rendered.
1876+
* 4. If a parent element is hidden, an element itself is not being rendered.
1877+
* (CSS visibility property and display property are inherited.)
1878+
*
1879+
* @see {@link https://html.spec.whatwg.org/multipage/rendering.html#being-rendered}
1880+
* @function isBeingRendered
1881+
* @param element {Node}
1882+
* @return {boolean}
1883+
*/
1884+
function isBeingRendered(element) {
1885+
if (!isVisibleStyleProperty(element.parentElement)) {
1886+
return false;
1887+
}
1888+
if (!isVisibleStyleProperty(element) || (element.style.opacity === '0') || (window.getComputedStyle(element).height === '0px' || window.getComputedStyle(element).width === '0px')) {
1889+
return false;
1890+
}
1891+
return true;
1892+
}
1893+
1894+
/**
1895+
* Determine if the element is visible for the user or not.
1896+
* 1. If an element sum of its offsetWidth, offsetHeight, height and width is less than 1 is not visible.
1897+
* 2. If elementCenter.x is less than is not visible.
1898+
* 3. If elementCenter.x is more than the document's width is not visible.
1899+
* 4. If elementCenter.y is less than 0 is not visible.
1900+
* 5. If elementCenter.y is the document's height is not visible.
1901+
*
1902+
* @function isVisible
1903+
* @param element {Node}
1904+
* @return {boolean}
1905+
*/
1906+
function isVisible(element) {
1907+
if ((element.offsetWidth + element.offsetHeight + element.getBoundingClientRect().height + element.getBoundingClientRect().width) === 0) {
1908+
return false;
1909+
}
1910+
1911+
// Define elementCenter object with props of x and y
1912+
// x: Left position relative to the viewport plus element's width (no margin) divided between 2.
1913+
// y: Top position relative to the viewport plus element's height (no margin) divided between 2.
1914+
const elementCenter = {
1915+
x: element.getBoundingClientRect().left + element.offsetWidth / 2,
1916+
y: element.getBoundingClientRect().top + element.offsetHeight / 2
1917+
};
1918+
1919+
if (elementCenter.x < 0) {
1920+
return false;
1921+
}
1922+
if (elementCenter.x > (document.documentElement.clientWidth || window.innerWidth)) {
1923+
return false;
1924+
}
1925+
if (elementCenter.y < 0) {
1926+
return false;
1927+
}
1928+
if (elementCenter.y > (document.documentElement.clientHeight || window.innerHeight)) {
1929+
return false;
1930+
}
1931+
1932+
let pointContainer = document.elementFromPoint(elementCenter.x, elementCenter.y);
1933+
1934+
while (pointContainer) {
1935+
if (pointContainer === element) {
1936+
return true;
1937+
}
1938+
if (pointContainer.parentNode) {
1939+
pointContainer = pointContainer.parentNode;
1940+
} else {
1941+
return false;
1942+
}
1943+
1944+
}
1945+
}
1946+
1947+
// If no DOM element was passed as argument use this component's element.
1948+
if (!el) {
1949+
el = this.el();
1950+
}
1951+
1952+
// If element is visible, is being rendered & either does not have a parent element or its tabIndex is not negative.
1953+
if (isVisible(el) && isBeingRendered(el) && ((!el.parentElement) || (el.tabIndex >= 0))) {
1954+
return true;
1955+
}
1956+
return false;
1957+
}
1958+
17681959
/**
17691960
* Register a `Component` with `videojs` given the name and the component.
17701961
*

src/js/menu/menu-button.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ class MenuButton extends Component {
308308
this.menuButton_.focus();
309309
}
310310
// Up Arrow or Down Arrow also 'press' the button to open the menu
311-
} else if (keycode.isEventKey(event, 'Up') || keycode.isEventKey(event, 'Down')) {
311+
} else if ((keycode.isEventKey(event, 'Up') || keycode.isEventKey(event, 'Down')) && !(this.player_.options_.playerOptions.spatialNavigation && this.player_.options_.playerOptions.spatialNavigation.enabled)) {
312312
if (!this.buttonPressed_) {
313313
event.preventDefault();
314314
this.pressButton();

src/js/modal-dialog.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const MODAL_CLASS_NAME = 'vjs-modal-dialog';
2121
class ModalDialog extends Component {
2222

2323
/**
24-
* Create an instance of this class.
24+
* Creates an instance of this class.
2525
*
2626
* @param { import('./player').default } player
2727
* The `Player` that this class should be attached to.
@@ -236,6 +236,7 @@ class ModalDialog extends Component {
236236
if (!this.opened_) {
237237
return;
238238
}
239+
239240
const player = this.player();
240241

241242
/**
@@ -265,8 +266,10 @@ class ModalDialog extends Component {
265266
*
266267
* @event ModalDialog#modalclose
267268
* @type {Event}
269+
*
270+
* @property {boolean} [bubbles=true]
268271
*/
269-
this.trigger('modalclose');
272+
this.trigger({type: 'modalclose', bubbles: true});
270273
this.conditionalBlur_();
271274

272275
if (this.options_.temporary) {
@@ -454,7 +457,13 @@ class ModalDialog extends Component {
454457
* @listens keydown
455458
*/
456459
handleKeyDown(event) {
457-
460+
/**
461+
* Fired a custom keyDown event that bubbles.
462+
*
463+
* @event ModalDialog#modalKeydown
464+
* @type {Event}
465+
*/
466+
this.trigger({type: 'modalKeydown', originalEvent: event, target: this, bubbles: true});
458467
// Do not allow keydowns to reach out of the modal dialog.
459468
event.stopPropagation();
460469

src/js/player.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {hooks} from './utils/hooks';
3636
import {isObject} from './utils/obj';
3737
import keycode from 'keycode';
3838
import icons from '../images/icons.svg';
39+
import SpatialNavigation from './spatial-navigation.js';
3940

4041
// The following imports are used only to ensure that the corresponding modules
4142
// are always included in the video.js package. Importing the modules will
@@ -562,6 +563,13 @@ class Player extends Component {
562563
this.addClass('vjs-audio');
563564
}
564565

566+
// Check if spatial navigation is enabled in the options.
567+
// If enabled, instantiate the SpatialNavigation class.
568+
if (options.spatialNavigation && options.spatialNavigation.enabled) {
569+
this.spatialNavigation = new SpatialNavigation(this);
570+
this.addClass('vjs-spatial-navigation-enabled');
571+
}
572+
565573
// TODO: Make this smarter. Toggle user state between touching/mousing
566574
// using events, since devices can have both touch and mouse events.
567575
// TODO: Make this check be performed again when the window switches between monitors
@@ -5447,6 +5455,10 @@ Player.prototype.options_ = {
54475455
responsive: false,
54485456
audioOnlyMode: false,
54495457
audioPosterMode: false,
5458+
spatialNavigation: {
5459+
enabled: false,
5460+
horizontalSeek: false
5461+
},
54505462
// Default smooth seeking to false
54515463
enableSmoothSeeking: false
54525464
};

src/js/slider/slider.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,32 @@ class Slider extends Component {
308308
* @listens keydown
309309
*/
310310
handleKeyDown(event) {
311-
312-
// Left and Down Arrows
313-
if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
311+
const spatialNavOptions = this.options_.playerOptions.spatialNavigation;
312+
const spatialNavEnabled = spatialNavOptions && spatialNavOptions.enabled;
313+
const horizontalSeek = spatialNavOptions && spatialNavOptions.horizontalSeek;
314+
315+
if (spatialNavEnabled) {
316+
if ((horizontalSeek && keycode.isEventKey(event, 'Left')) ||
317+
(!horizontalSeek && keycode.isEventKey(event, 'Down'))) {
318+
event.preventDefault();
319+
event.stopPropagation();
320+
this.stepBack();
321+
} else if ((horizontalSeek && keycode.isEventKey(event, 'Right')) ||
322+
(!horizontalSeek && keycode.isEventKey(event, 'Up'))) {
323+
event.preventDefault();
324+
event.stopPropagation();
325+
this.stepForward();
326+
} else {
327+
super.handleKeyDown(event);
328+
}
329+
330+
// Left and Down Arrows
331+
} else if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
314332
event.preventDefault();
315333
event.stopPropagation();
316334
this.stepBack();
317335

318-
// Up and Right Arrows
336+
// Up and Right Arrows
319337
} else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
320338
event.preventDefault();
321339
event.stopPropagation();

0 commit comments

Comments
 (0)