Sometimes, it is useful to differentiate between physical and virtual keyboards in a web application. In my case, I had built a new messaging system for Yanteres with a responsive design which could be used on any device. While responsive design is a given on today’s web, the behavior of certain actions – which can’t be controlled via CSS – is different on mobile vs desktop. In the case of the aforementioned messaging system, hitting Return on physical keyboards is expected to send a message, while hitting Return on a phone’s or tablet’s virtual keyboard is expected to insert a newline.

Screen width media queries won’t cut it

On first thought, one may be tempted to use a media query in JavaScript to change the behavior for mobile vs desktop. However, let’s not forget that the screen size isn’t what changes the behavior here. A mobile phone or tablet can be hooked up to a physical keyboard and a desktop computer can also have a touch screen. Also, in landscape mode, a tablet resolution is essentially that of a small desktop in most responsive designs.

We need to differentiate between a physical and a virtual keyboard rather than screen size, and browsers currently have no API to do so that I am aware of. Searching high and low, I’ve found many people asking for a way to change the layout in response to the presence of a virtual keyboard, or lack thereof, but only one discussion on the actual behavior of the keyboard, which had no solution. Then I had a thought – does a virtual keyboard care about when you press and release keys the way a physical keyboard does, or does it fire keydown and keyup events instantaneously?

Counting key press and release time

I got the hunch that perhaps virtual keyboards fire the events in rapid succession in a way that was impossible for humans, and immediately went on to test my hunch on iOS and Android devices and desktop browsers by binding the keydown and keyup events that would log the time difference between key press and release.

Note: I use jQuery and CoffeeScript in my examples, but the concepts can be applied in vanilla JS easily. Update : I’m migrating away from CoffeeScript, so I’ve updated the code to JavaScript.

var interceptEnterKey = function interceptEnterKey ($message) {
  var start = 0                       // initialize var start

  $message.keydown(function () {
    start = new Date().getTime()      // set start to current time
  })

  $message.keyup(function () {
    console.log(new Date().getTime() - start) // log the difference
  })
}

var initTextArea = function () {
  var $message = $('#message_body')
  interceptEnterKey($message)
}

$(initTextArea)

The results were pretty much as I had expected. Virtual keyboards on mobile devices fire the events at physically impossible speeds.

Keyboard Speed Test Results

When it came to Windows and Linux, I got pretty much the same results as with Mac, Android was slightly slower, and I didn’t get to test it on Windows touch devices (if anyone wants to help with that, please do). In summary, physical keyboards almost always returned over 30ms – even with quickly jabbing the keys, iOS virtual keyboards always took less than 10ms, and Android virtual keyboards were around 12ms.

Key press and release times are not 100% consistent

So, the answer is to count the milliseconds between keyup and keydown and you shall know whether or not you have a virtual or physical keyboard, right? Unfortunately, one key press and release won’t always give you the right answer. In some cases, a physical keyboard will have an odd key press that lasts less than 10ms (pictured above) and Android will have an odd one that lasts up to 25ms. The solution I came up with was to keep count of each key press time and then to find the average when I needed it. In my application, that would be when the user presses the Return key. If the average time is over 25ms, chances are good that it’s a physical keyboard.

var interceptEnterKey = function interceptEnterKey ($message) {
  var start = 0                       // initialize var start
  var keyTimes = []                   // array to store times

  $message.keydown(function (e) {     // we will be using the event now
    start = new Date().getTime()      // set start to current time

    if (e.keyCode == 13) {            // if return was pressed
      if (!keyTimes.length) {         // if it was the first key pressed
        e.preventDefault()            // cancel newline insertion and
        return                        // exit function immediately
      }

      // Note: Array.reduce is an ES5 function.
      // Use a polyfill if you need IE8 support.
                                                      // sum up the times
      var sumKeyTime = keyTimes.reduce(function (x, y) { return x + y })
      var avgKeyTime = sumKeyTime / keyTimes.length   // find the average time

      if (avgKeyTime > 25) {          // match physical keyboards
        e.preventDefault()            // cancel newline insertion
        keyTimes = []                 // reset the key press times
        $(this.form).submit()         // send the message
      }
    }
  })

  $message.keyup(function () {
    keyTimes.push(new Date().getTime() - start) // store the difference
  })
}
// ...

Newlines with physical keyboards

The last step for a proper messaging system is to allow users of physical keyboards to enter newlines using Shift/Option/Meta/Etc + Return. All that needs to be done here is to check whether those keys are pressed on the keydown event, and we’re done! I didn’t want a very large if statement, so I put it in its own function.

var isKeyRawEnter = function isKeyRawEnter (e) {
  return !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && e.keyCode === 13
}

// ...
-    if (e.keyCode == 13) {            // if return was pressed
+    if (isKeyRawEnter(e)) {           // if return was pressed

TL;DR

The average time difference between keydown and keyup can be a good indicator of whether a user is typing on a physical or virtual keyboard. Here is the full code to an example that sends the message when Return is pressed on a physical keyboard and adds a newline when pressed on a virtual keyboard.

var isKeyRawEnter = function isKeyRawEnter (e) {
  return !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && e.keyCode === 13
}

var interceptEnterKey = function interceptEnterKey ($message) {
  var start = 0                       // initialize var start
  var keyTimes = []                   // array to store times

  $message.keydown(function (e) {     // event to determine key(s)
    start = new Date().getTime()      // set start to current time

    if (isKeyRawEnter(e)) {           // if return was pressed
      if (!keyTimes.length) {         // if it was the first key pressed
        e.preventDefault()            // cancel newline insertion and
        return                        // exit function immediately
      }

      // Note: Array.reduce is an ES5 function.
      // Use a polyfill if you need IE8 support.
                                                      // sum up the times
      var sumKeyTime = keyTimes.reduce(function (x, y) { return x + y })
      var avgKeyTime = sumKeyTime / keyTimes.length   // find the average time

      if (avgKeyTime > 25) {          // match physical keyboards
        e.preventDefault()            // cancel newline insertion
        keyTimes = []                 // reset the key press times
        $(this.form).submit()         // send the message
      }
    }
  })

  $message.keyup(function () {
    keyTimes.push(new Date().getTime() - start) // store the difference
  })
}

var initTextArea = function initTextArea () {
  $message = $('#message_body')
  interceptEnterKey($message)
}

$(initTextArea)
<form id="message-form" method="post" action="/messages">
  <div>
    <textarea id="message_body" name="message[body]"></textarea>
  </div>
  <div>
    <button type="submit">Send</button>
  </div>
</form>

Demo


lyosha forum