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>
    </bp-directional>
</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() {
   ...