Skip to content

Instantly share code, notes, and snippets.

@Cornally
Last active August 18, 2020 13:24
Show Gist options
  • Save Cornally/62265bd36b715687032d3eab3cbee835 to your computer and use it in GitHub Desktop.
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.
// 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