Vue directive (Vue.js 2.x) for spatial navigation (keyboard navigation)
npm install vue-spatialnavigation --save
Spatial navigation is keyboard navigation of a html page most often used on devices where mouse navigation is not available or not preferred. An important rule is that only 1 element can have the active 'focus' state at the same time.
When a user navigates the page with the left, right, up, down keybaord keys a decision should be made which element to focus next. This directive will improve the Vue native keyboard binding to provide a flexible spatial navigation implementation.
Inspiration for this plugin is taken from the CSS3 draft keyboard navigation, a limitation is that CSS3 can only point the focus position based on (DOM) element id pointers, while this directive supports both element id pointers and custom (component) method binding.
To make an element 'focusable' just add the focus directive
import Vue from "vue";
import VueSpatialNavigation from "vue-spatialnavigation";
Vue.use(VueSpatialNavigation)
<Component id="button1" v-focus></Component>
<Component id="button2" v-focus></Component>
Use data properties with a domId string reference to determine the focus action during navigation (this behaves similar to the CSS3 draft)
<Component id="button1" v-focus data-down="button2"></Component>
<Component id="button2" v-focus data-up="button1"></Component>
Use vue bind directive with a component function reference to determine the focus action during navigation. Note that the available Vue events (@down, @up, @right, @left, @click) only work on Vue components not on native html elements!
<Component id="button1" v-focus @down="componentFunctionDown"></Component>
<Component id="button2" v-focus @up="componentFunctionUp" ></Component>
import { navigationService } from "vue-spatialnavigation";
import Vue from "vue";
let app = new Vue({
methods: {
componentFunctionDown: () => {
let el = navigationService.getFocusElementById("button2");
if (el) el.focus()
},
componentFunctionUp: () => {
let el = navigationService.getFocusElementById("button1");
if (el) el.focus()
}
}
})
Use the special constant value 'AUTOFOCUS' to let the directive try to find the next focusable sibbling automatically. This only works if the next/prev focusable element is a sibling within the parent container!
<Component id="button1" v-focus data-down="AUTOFOCUS"></Component>
<Component id="button2" v-focus data-up="AUTOFOCUS"></Component>
In case the page is just rendered and or focus is reset the directive should know which element to restore focus to. The directive will keep a reference to the last focussed element, but if that is not available it will restore focus to the 'default' focus element on the page.
<Component id="button1" v-focus data-default data-down="button2"></Component>
<Component id="button2" v-focus data-up="button1"></Component>
When a user actions an 'enter' keypress it will automatically be converted into a click action on the element that has active focus. So you can use the regular vue @click bindings. Note that by default vue only supports @click on html elements and you have to normally use @click.native on a component, this directive will automatically capture the native event though and convert it into a vue event so you can use @click for both native keyboard and mouse clicks.
<Component id="button1" v-focus @down="componentFunctionDown"></Component>
<Component id="button2" v-focus @up="componentFunctionUp" @click="button2clicked"></Component>
import { navigationService } from "vue-spatialnavigation";
import Vue from "vue";
let app = new Vue({
methods: {
componentFunctionDown: () => {
let el = navigationService.getFocusElementById("button2");
if (el) el.focus()
},
componentFunctionUp: () => {
let el = navigationService.getFocusElementById("button1");
if (el) el.focus()
},
button2clicked: () => {
console.log("button2clicked");
}
}
})
The application uses a default keybinding for the d-pad and enter keys, during the directive initialisation this can optionally be overriden with the options object:
import Vue from "vue";
import VueSpatialNavigation from "vue-spatialnavigation";
Vue.use(VueSpatialNavigation, {
keyCodes: {
"up": 38,
"down": 40,
"left": 37,
"right": 39,
"enter": 13
}
})
It is also possible to override the NavigationService class to create a fully custom key binding.
import Vue from "vue";
import VueSpatialNavigation from "vue-spatialnavigation";
import { NavigationService } from "vue-spatialnavigation/lib/navigation.service";
// custum navigation service
class CustomNavigationService extends NavigationService {
}
Vue.use(VueSpatialNavigation, {
navigationService: CustomNavigationService
})
The directive will automatically assigned and remove the class "focus" on the html element to distingish an element in focus vs an element not in focus. The css "focus" class handling is fully handled by the directive and should not be changed/set manually.
<style>
.focus { background-color: yellow;}
</style>
<Component id="button1" v-focus data-default data-down="AUTOFOCUS"></Component>
<Component id="button2" v-focus data-up="AUTOFOCUS"></Component>
In addition to the directive this plugin also exposes a NavigationService object that allows control of the focusable elements from JS code. The navigation service keeps track of all focusable elements on the page and can be used to query focusable elements/
It provides the below properties:
blockAllSpatialNavigation: boolean; // enabled / disable a navigation block (e.g. during loading)
It provides the below methods:
NavigationService.getFocusElementInFocus(): FocusElement | undefined; // get FocusElement currently in focus
NavigationService.getFocusElementById(id: string): FocusElement | undefined; // get FocusElement by DOM id
NavigationService.getFocusElementIsDefault(): FocusElement | undefined; // get FocusElement by default property
NavigationService.blurAllFocusElements(): void; // blur all FocusElements
FocusElement is a class initialised for each dom element where the directive is applied, you can aquire the FocusElement by quering the NavigationService.
It provides the below properties:
FocusElement.id: string; // dom id reference
FocusElement.isFocus: boolean; // is element currently focussed
FocusElement.isDefault: boolean; // is element a default focusable element
And the below properties:
FocusElement.focus(): void; // manually focus the current element
FocusElement.blur(): void; // manually blur the current element
FocusElement.left(): void; // manually trigger left action the current element
FocusElement.right(): void; // manually trigger right action the current element
FocusElement.up(): void; // manually trigger up action the current element
FocusElement.down(): void; // manually trigger down action the current element
FocusElement.enter(): void; // manually trigger enter action the current element