DEV Community

Pacharapol Withayasakpunt
Pacharapol Withayasakpunt

Posted on

Keyboard control of your web app (beyond tabindex)

In vanilla JS, of course, but also compatible with SPA.

The trick is to attach to window's onkeydown / onkeypress / onkeyup, and remove the handler when needed. (Using addEventListener / removeEventListener is OK too, but harder to prevent from attaching multiple times.)

  async startQuiz() {
    window.onkeypress = this.onQuizKeypress.bind(this)
    this.isQuizModal = true
  }

  endQuiz() {
    window.onkeypress = null
    this.isQuizModal = false
  }

  beforeDestroy() {
    window.onkeypress = null
  }
Enter fullscreen mode Exit fullscreen mode

If you use class, function keyword or this keyword, you would also need to .bind(this).

About the real event, you might need several escaping mechanisms, before the real thing.

  onQuizKeypress(evt: KeyboardEvent) {
    if (!this.isQuizModal) {
      return
    }

    if (
      evt.target instanceof HTMLTextAreaElement ||
      evt.target instanceof HTMLInputElement
    ) {
      return
    }

    ...
  }
Enter fullscreen mode Exit fullscreen mode

Add several utility functions,

    const click = async (el: any) => {
      if (!el) return

      if (el.classList?.add) {
        el.classList.add('active')
      }

      if (el.click) {
        const r = el.click()
        if (r instanceof Promise) {
          await r
          if (el.classList?.remove) {
            el.classList.remove('active')
          }
        }
        // else {
        //   await new Promise((resolve) => setTimeout(resolve, 500))
        //   if (el.classList?.remove) {
        //     el.classList.remove('active')
        //   }
        // }
      }
    }

    const mapKey = (map: Record<string, (() => any) | string>) => {
      let action = map[evt.key]
      while (typeof action === 'string') {
        action = map[action]
      }
      if (typeof action === 'function') action()
    }
Enter fullscreen mode Exit fullscreen mode

Now, for the real key mapping,

  onQuizKeypress(evt: KeyboardEvent) {
    ... // Escape several things

    mapKey({
      '1': () => click(this.$refs.btnMarkRight),
      '2': () => click(this.$refs.btnMarkWrong),
      '3': () => click(this.$refs.btnMarkRepeat),
      q: () => click(this.$refs.btnHideAnswer),
      ' ': 'q',
      e: () => click(this.$refs.btnEditModal),
    })
  }
Enter fullscreen mode Exit fullscreen mode

And now, for the styling. I just make it simple, yet clear.

button:focus, button:active, button.active {
  filter: grayscale(50%) saturate(75%);
}
Enter fullscreen mode Exit fullscreen mode

If you are curious, I use this a real project -- patarapolw/zhquiz -- packages/nuxt/pages/quiz.vue. Visit my web app.

Top comments (0)