Vue
by khamer
HTML
<main id="app">
<key-layer>
<ul style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));">
<li>
<a href="#"><span>Consectetur non laborum!</span></a>
</li>
<li>
<a href="#"><span>Adipisicing natus laboriosam?</span></a>
</li>
<li>
<a href="#"><span>Dolor accusamus quis</span></a>
</li>
<li>
<a href="#"><span>Elit iste amet.</span></a>
</li>
<li>
<a href="#"><span>Elit nulla provident?</span></a>
</li>
<li>
<a href="#"><span>Adipisicing sit quae?</span></a>
</li>
<li>
<a href="#"><span>Dolor quisquam explicabo.</span></a>
</li>
<li>
<a href="#"><span>Adipisicing molestiae facilis.</span></a>
</li>
<li>
<a href="#"><span>Ipsum officiis ipsa?</span></a>
</li>
<li>
<a href="#"><span>Lorem possimus nam.</span></a>
</li>
<li>
<a href="#"><span>Adipisicing provident ipsum</span></a>
</li>
<li>
<a href="#"><span>Amet odio repudiandae.</span></a>
</li>
<li>
<a href="#"><span>Consectetur impedit praesentium!</span></a>
</li>
<li>
<a href="#"><span>Dolor sunt praesentium</span></a>
</li>
<li>
<a href="#"><span>Adipisicing molestias quis</span></a>
</li>
<li>
<a href="#"><span>Adipisicing aspernatur cum.</span></a>
</li>
<li>
<a href="#"><span>Dolor numquam aut.</span></a>
</li>
</ul>
</key-layer>
</main>
CSS
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
del {
color: rgba(0, 0, 0, 0.3);
}
li a {
display: block;
}
Vue
const FOCUSABLE_SELECTOR = [
'a[href]',
'audio[controls]',
'button',
'details summary',
'input',
'map area[href]',
'select',
'svg a[xlink\\:href]',
'[tabindex]',
'textarea',
'video[controls]',
].map(t => t + ':not([tabindex^="-"]):not([disabled])').join();
const KEY_FILTERS = {
ArrowRight: ({x, y}) => x > 0 && Math.abs(x) > Math.abs(y),
ArrowLeft: ({x, y}) => x < 0 && Math.abs(x) > Math.abs(y),
ArrowDown: ({x, y}) => y > 0 && Math.abs(x) < Math.abs(y),
ArrowUp: ({x, y}) => y < 0 && Math.abs(x) < Math.abs(y),
}
Vue.component('key-layer', {
methods: {
focusable() {
return this.$el.querySelectorAll(FOCUSABLE_SELECTOR)
},
getCoordinates(el) {
return {
x: el.offsetLeft + el.offsetWidth / 2,
y: el.offsetTop + el.offsetHeight / 2
}
},
withCoordinates(nodeList, origin) {
const elements = [];
const {x: originX, y: originY} = this.getCoordinates(origin);
nodeList.forEach(el => {
let {x, y} = this.getCoordinates(el);
x -= originX
y -= originY
el.setAttribute('data-xy', `${x} ${y}`)
elements.push({ el, x, y, d: x*x + y*y })
})
return elements
},
findTarget(el, key) {
const elements = this.withCoordinates(this.focusable(), el)
return elements
.filter(key in KEY_FILTERS ? KEY_FILTERS[key] : x => x)
.reduce((closest, n) => n.d < closest.d ? n : closest, { d: Infinity })
.el
},
keydown(evt) {
const newTarget = this.findTarget(evt.target, evt.key)
if (newTarget) {
evt.stopPropagation()
newTarget.focus()
return false
}
return true
}
},
mounted() {
...