Last active
August 18, 2020 13:24
-
-
Save Cornally/62265bd36b715687032d3eab3cbee835 to your computer and use it in GitHub Desktop.
The following outlines an approach I've used for tackling navigation in web-based Smart TV applications. Some assembly required.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Inside each componenent, I defined navigation targets inline on the respective `<template>` elements: | |
<input | |
v-bind:value="value" | |
type="text" | |
rel="search-input" | |
:key-navigation-selectable="JSON.stringify(setSearchTargets())" | |
/> | |
// Potential targets are mapped out in a method below: | |
setSearchTargets: function(idx) { | |
return { | |
'up': [TARGET.MENU.FAVORITES], | |
'down': [this.previousSelectable, TARGET.CARD_FIRST, TARGET.BUTTON.ADD_FAVORITE], | |
'left': [TARGET.MENU.CHANNELS], | |
'right': null | |
} | |
} | |
// Utilize a VueJS directive for most of the lifting: | |
import Vue from 'vue'; | |
import { debounce } from 'lodash'; | |
import { KEYS, TARGET } from '@/directives/key-navigation-helper'; | |
import $store from '@/store'; //import Vuex from 'vuex' | |
Vue.directive('key-navigation', { | |
bind: function(el, binding, vnode) { | |
function getActiveTarget(activeSelectable, direction) { | |
if (activeSelectable === 'video') return; | |
let el = document.querySelector(activeSelectable) || document.querySelector(TARGET.MENU.CHANNELS); | |
let targets = JSON.parse(el.getAttribute('key-navigation-selectable')); | |
// Ensure the target element is visible. | |
if (targets[direction]) { | |
let targetDirection = targets[direction].filter((target) => document.querySelectorAll(target).length); | |
return targetDirection[0]; | |
} | |
} | |
function move(direction) { | |
let activeSelectable = $store.getters.activeSelectable; | |
let newSelectable = getActiveTarget(activeSelectable, direction); | |
if (newSelectable) $store.dispatch('updateSelectable', { active: newSelectable }); | |
} | |
// Debounce to avoid crushing card scroller animation | |
let handleKeypress = debounce(function(e) { | |
let key = e.keyCode; | |
switch (key) { | |
case KEYS['left']: | |
move('left'); | |
break; | |
case KEYS['up']: | |
move('up'); | |
break; | |
case KEYS['right']: | |
move('right'); | |
break; | |
case KEYS['down']: | |
move('down'); | |
break; | |
} | |
}, 75); | |
document.addEventListener('keydown', handleKeypress); | |
} | |
}); | |
// <App /> receives this directive: | |
<template> | |
<div id="app" v-key-navigation="activeSelectable"> | |
<div class="container"> | |
<div class="container__left"> | |
<img class="app__logo" src="./assets/logo.svg"> | |
<main-menu></main-menu> | |
</div> | |
<div class="container__center"> | |
<router-view ref="view"></router-view> | |
</div> | |
<transition name="alert" mode="out-in"> | |
<alert v-if="alert.visible" :alert-title="alert.title" :alert-body="alert.body"></alert> | |
</transition> | |
</div> | |
</div> | |
</template> | |
// Contents of key-navigation-helper: | |
// Parent regions whose children we traverse | |
export const PARENT = { | |
LEFT: '.container__left', | |
CENTER: '.container__center' | |
} | |
// Child COMPONENTS which can gain focus | |
export const TARGET = { | |
BUTTON: { | |
ADD_FAVORITE: '.btn--add-favorite', | |
REMOVE: '.btn--remove', | |
GO_BACK: '.btn--go-back' | |
}, | |
CARD_FIRST: '.card-0', | |
CARD: '.card', | |
MENU: { | |
CHANNELS: '.main-menu__channels', | |
HISTORY: '.main-menu__history', | |
FAVORITES: '.main-menu__favorites', | |
}, | |
SEARCH: '.search-bar__input', | |
VIDEO: '.video' | |
} | |
// Common key mappings for remotes (XXX: LG specific?) | |
export const KEYS = { | |
'left' : 37, | |
'up' : 38, | |
'right' : 39, | |
'down' : 40, | |
'enter' : 13, | |
'stop' : 413, | |
'pause' : 19, | |
'play' : 415, | |
'rewind' : 412, | |
'fastforward': 417, | |
'esc' : 27 | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment