Monday, August 12, 2013

KeyName is a Key Name in Dart


Last night I was wildly successful in extracting a keyboard interaction library out of my ICE Code Editor codebase. Thanks mostly to Dart being an amazing language, I was able to create a new library in a single night, including writing tests, some documentation, and pushing to both GitHub (ctrl-alt-foo repository) and Dart's Pub package repository (ctrl_alt_foo package). And best of all: the many, many tests in original ICE codebase never broke.

Until I made one last change and pushed everything. Of course I did that.

The failing tests in the original repository all have to do with “special” key events like Enter, Escape and the Arrow keys. It seems that all of the keyboard event helpers in the new library are working through the type() method:
hitEnter()=> type(KeyName.ENTER);
hitEscape()=> type(KeyName.ESC);

type(String key) {
  document.activeElement.dispatchEvent(
    new KeyboardEvent(
      'keydown',
      keyIdentifier: keyIdentifierFor(key)
    )
  );
}
The keyIdentifier parameter in the KeyboardEvent's constructor is an odd duck. Not surprisingly, it is causing the failures. As of this writing, the keyIdentifier property is the only way to supply data to a keyboard event in Dart. Unfortunately, there is very little support for the keyIdentifier property in JavaScript or even in Dart itself. This makes it difficult to generate events that are cross-browser compatible or even reliable in Dart. This is the crux of the new ctrl_alt_foo package: it ensures that keyIdentifier is cross-browser compatible and reliable in Dart.

My problem was the keyIdentifierFor() function which prepares the key press into a testable/cross-browser state:
String keyIdentifierFor(char) {
  if (char.codeUnits.length != 1) throw "Don't know how to type “$char”";

  // Keys are uppercase (see Event.keyCode)
  var key = char.toUpperCase();

  return 'U+00' + key.codeUnits.first.toRadixString(16).toUpperCase();
}
Weirdly enough, the cross-browser state is a string representation of the Unicode value of the character (e.g. "U+0041"). My problem is that this code will only work with simple, single-byte code. It will not work with multi-byte strings, which is what the KeyName.Enter string is.

But this is a perfect excuse to write another test! Yay!

The ctrl_alt_foo package exposes a keyboard stream named KeyboardEventStreamX. In my test, I establish such a listener, simulate Enter with the hitEnter() helper, and expect that the resultant event will have the isEnter predicate method set to true:
  test("can listen for Enter keys", (){
    KeyboardEventStreamX.onKeyDown(document).listen(expectAsync1((e) {
      expect(e.isEnter, true);
    }));

    hitEnter();
  });
That fails with the thrown string, just as my ICE tests are currently failing:
ERROR: can listen for Enter keys
  Test failed: Caught Don't know how to type “Enter” 
The solution to this particular failure turns out to be full of “ugh.”

The problem was mostly mine. I was under the impression that the KeyName class in Dart was string representations of the various special keys. That is, I expected that KeyName.ENTER was a non-printable string of 0x0a embedded into a string. It turns out that KeyName.ENTER is the string "Enter". That might seem somewhat useless, but it serves as a constant lookup for the values in the KeyboardEvent DOM object.

It serves as little use to me in this case, so I have to build my own conversion class:
class KeyIdentifier {
  static final map = {
    'Backspace': 'U+0008',
    'Tab':       'U+0009',
    'Enter':     'U+000A',
    'Esc':       'U+001B',
    'Del':       'U+007F',
    'Cancel':    'U+0018',
    'Spacebar':  'U+0020',
    'Tab':       'U+0009',
    'Del':       'U+007F',
    'Left':      'U+0025',
    'Up':        'U+0026',
    'Right':     'U+0027',
    'Down':      'U+0028'
  };

  static forKeyName(name)=> map[name];
}
Using those crazy Unicode values and the conversion facilities from previous nights, I am able to properly generate an Enter keyboard event.

Now that I think about it, I am not entirely certain that these special keyboard events are working cross-browser. I *think* they ought to be OK, but I will double check that tomorrow. If that is working, then it is time to add support for Mac keyboard shortcuts (stupid command key).


Day #841

No comments:

Post a Comment