From 662f0c6d4fa0f8719467bb35eb6da02a64bad8a6 Mon Sep 17 00:00:00 2001
From: Volker Braun <vbraun.name@gmail.com>
Date: Tue, 3 May 2016 16:47:53 +0200
Subject: [PATCH] fix(chips): keypress handler for chips separator keys

Chips separator keys can now either be numeric (and matched in the
keydown handler) or string (and matched in the keypress handler).

Closes #8319
---
 src/components/chips/chips.spec.js                 | 64 ++++++++++++++++++++--
 .../chips/demoCustomSeparatorKeys/script.js        | 13 +++--
 src/components/chips/js/chipsController.js         | 25 ++++++++-
 src/components/chips/js/chipsDirective.js          |  5 +-
 4 files changed, 96 insertions(+), 11 deletions(-)

diff --git a/src/components/chips/chips.spec.js b/src/components/chips/chips.spec.js
index 217fb14..2912a6d 100755
--- a/src/components/chips/chips.spec.js
+++ b/src/components/chips/chips.spec.js
@@ -892,26 +892,80 @@ describe('<md-chips>', function() {
           '<md-chips ng-model="items" md-separator-keys="keys"></md-chips>';
 
         it('should create a new chip when a comma is entered', inject(function($mdConstant) {
-          scope.keys = [$mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.COMMA];
+          scope.keys = [$mdConstant.KEY_CODE.ENTER, ','];
           var element = buildChips(SEPARATOR_KEYS_CHIP_TEMPLATE);
           var ctrl = element.controller('mdChips');
 
-          var commaInput = {
+          // Event sequence when pressing the comma key: first commaKeydown, then commaKeypress
+          var commaAscii = ','.charCodeAt(0);
+          var commaKeydown = {
             type: 'keydown',
             keyCode: $mdConstant.KEY_CODE.COMMA,
+            charCode: 0,
             which: $mdConstant.KEY_CODE.COMMA,
             preventDefault: jasmine.createSpy('preventDefault')
           };
+          var commaKeypress = {
+            type: 'keypress',
+            keyCode: commaAscii,
+            charCode: commaAscii,
+            which: commaAscii,
+            preventDefault: jasmine.createSpy('preventDefault')
+          };
+
+          ctrl.chipBuffer = 'Test';
+          element.find('input').triggerHandler(commaKeydown);
+          element.find('input').triggerHandler(commaKeypress);
+
+          expect(commaKeydown.preventDefault).not.toHaveBeenCalled();
+          expect(commaKeypress.preventDefault).toHaveBeenCalled();
+        }));
+
+        it('should not create a new chip when shift+comma is pressed', inject(function($mdConstant) {
+          scope.keys = [$mdConstant.KEY_CODE.ENTER, ','];
+          var element = buildChips(SEPARATOR_KEYS_CHIP_TEMPLATE);
+          var ctrl = element.controller('mdChips');
+
+          // Event sequence when pressing the comma key:
+          // first shiftKeydown, then commaKeydown, then shiftCommaKeypress
+          var shiftKeyCode = 16;
+          var commaAscii = ','.charCodeAt(0);
+          var shiftCommaAscii = '<'.charCodeAt(0);  // depends on keyboard, example is dvorak
+          var shiftKeydown = {
+            type: 'keydown',
+            keyCode: shiftKeyCode,
+            charCode: 0,
+            which: shiftKeyCode,
+            preventDefault: jasmine.createSpy('preventDefault')
+          };
+          var commaKeydown = {
+            type: 'keydown',
+            keyCode: $mdConstant.KEY_CODE.COMMA,
+            charCode: 0,
+            which: $mdConstant.KEY_CODE.COMMA,
+            preventDefault: jasmine.createSpy('preventDefault')
+          };
+          var shiftCommaKeypress = {
+            type: 'keypress',
+            keyCode: shiftCommaAscii,
+            charCode: shiftCommaAscii,
+            which: shiftCommaAscii,
+            preventDefault: jasmine.createSpy('preventDefault')
+          };
 
           ctrl.chipBuffer = 'Test';
-          element.find('input').triggerHandler(commaInput);
+          element.find('input').triggerHandler(shiftKeydown);
+          element.find('input').triggerHandler(commaKeydown);
+          element.find('input').triggerHandler(shiftCommaKeypress);
 
-          expect(commaInput.preventDefault).toHaveBeenCalled();
+          expect(shiftKeydown.preventDefault).not.toHaveBeenCalled();
+          expect(commaKeydown.preventDefault).not.toHaveBeenCalled();
+          expect(shiftCommaKeypress.preventDefault).not.toHaveBeenCalled();
         }));
 
         it('supports custom separator key codes', inject(function($mdConstant) {
           var semicolon = 186;
-          scope.keys = [$mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.COMMA, semicolon];
+          scope.keys = [$mdConstant.KEY_CODE.ENTER, ',', semicolon];
 
           var element = buildChips(SEPARATOR_KEYS_CHIP_TEMPLATE);
           var ctrl = element.controller('mdChips');
diff --git a/src/components/chips/demoCustomSeparatorKeys/script.js b/src/components/chips/demoCustomSeparatorKeys/script.js
index a389b22..38fb923 100644
--- a/src/components/chips/demoCustomSeparatorKeys/script.js
+++ b/src/components/chips/demoCustomSeparatorKeys/script.js
@@ -5,13 +5,18 @@
       .controller('CustomSeparatorCtrl', DemoCtrl);
 
   function DemoCtrl ($mdConstant) {
-    // Use common key codes found in $mdConstant.KEY_CODE...
-    this.keys = [$mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.COMMA];
+    // Use common numeric key codes found in $mdConstant.KEY_CODE or
+    // length-one strings for characters
+    this.keys = [$mdConstant.KEY_CODE.ENTER, ','];
     this.tags = [];
 
-    // Any key code can be used to create a custom separator
+    // Any key code can be used to create a custom separator. Note
+    // that key codes ignore modifiers, so this makes shift+semicolon
+    // a separator, too
     var semicolon = 186;
-    this.customKeys = [$mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.COMMA, semicolon];
+    // Use this instead if you only want semicolon as separator
+    // var semicolon = ';';
+    this.customKeys = [$mdConstant.KEY_CODE.ENTER, ',', semicolon];
     this.contacts = ['test@example.com'];
   }
 })();
diff --git a/src/components/chips/js/chipsController.js b/src/components/chips/js/chipsController.js
index 9f34dcf..fe83d46 100644
--- a/src/components/chips/js/chipsController.js
+++ b/src/components/chips/js/chipsController.js
@@ -232,6 +232,7 @@ MdChipsCtrl.prototype.setupWrapperAria = function() {
  * @param event
  */
 MdChipsCtrl.prototype.inputKeydown = function(event) {
+  console.log('keydown', event);
   var chipBuffer = this.getChipBuffer();
 
   // If we have an autocomplete, and it handled the event, we have nothing to do
@@ -256,13 +257,34 @@ MdChipsCtrl.prototype.inputKeydown = function(event) {
     return;
   }
 
+  this.inputKey(event.keyCode, event);
+}
+
+/**
+ * Handles the keypress event on the input element: Search for special
+ * separator keys.
+ * @param event
+ */
+MdChipsCtrl.prototype.inputKeypress = function(event) {
+  console.log('keypress', event);
+  this.inputKey(String.fromCharCode(event.charCode), event);
+}
+
+/**
+ * Common code for both inputKeydown and inputKeypress
+ * @param key Either a numeric keyCode (keydown) or a length-one string (keypress)
+ * @param event
+ */
+MdChipsCtrl.prototype.inputKey = function(key, event) {
+  var chipBuffer = this.getChipBuffer();
+
   // By default <enter> appends the buffer to the chip list.
   if (!this.separatorKeys || this.separatorKeys.length < 1) {
     this.separatorKeys = [this.$mdConstant.KEY_CODE.ENTER];
   }
 
   // Support additional separator key codes in an array of `md-separator-keys`.
-  if (this.separatorKeys.indexOf(event.keyCode) !== -1) {
+  if (this.separatorKeys.indexOf(key) !== -1) {
     if ((this.autocompleteCtrl && this.requireMatch) || !chipBuffer) return;
     event.preventDefault();
 
@@ -742,6 +764,7 @@ MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
   inputElement
       .attr({ tabindex: 0 })
       .on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown) })
+      .on('keypress', function(event) { scopeApplyFn(event, ctrl.inputKeypress) })
       .on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus) })
       .on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur) })
 };
diff --git a/src/components/chips/js/chipsDirective.js b/src/components/chips/js/chipsDirective.js
index 428bba0..94079d0 100644
--- a/src/components/chips/js/chipsDirective.js
+++ b/src/components/chips/js/chipsDirective.js
@@ -133,7 +133,9 @@
    *    the delete key will remove the chip.
    * @param {string=} delete-button-label A label for the delete button. Also hidden and read by
    *    screen readers.
-   * @param {expression=} md-separator-keys An array of key codes used to separate chips.
+   * @param {expression=} md-separator-keys An array of keys used to separate chips. Each entry is
+   *     either a numeric key code (triggering on keydown, ignoring modifiers) or a length-one
+   *     string (matching that keypress).
    * @param {string=} md-chip-append-delay The number of milliseconds that the component will select
    *    a newly appended chip before allowing a user to type into the input. This is **necessary**
    *    for keyboard accessibility for screen readers. It defaults to 300ms and any number less than
@@ -221,6 +223,7 @@
             ng-model="$mdChipsCtrl.chipBuffer"\
             ng-focus="$mdChipsCtrl.onInputFocus()"\
             ng-blur="$mdChipsCtrl.onInputBlur()"\
+            ng-keypress="$mdChipsCtrl.inputKeypress($event)"\
             ng-keydown="$mdChipsCtrl.inputKeydown($event)">';
 
   var CHIP_DEFAULT_TEMPLATE = '\
-- 
2.10.2

