From a0ceb82bdee0088cfd233984fb753703502e1102 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 21 Sep 2015 17:55:54 +0300 Subject: [PATCH 1/4] fix(input): re-validate when partially editing a date-family input In date-family input types (`date`, `datetime-local`, `month`, `time`, `week`), the user can interact with parts of the value (e.g. year, month, hours etc) independently. Neverhteless, the actual value of the element is empty (`''`) unless all parts are filled (and valid). Thus, editing a signle part of the value may result in a change in the validity state of the `` (see below), without an accompanying change in the actual value of the element. In such cases, no `input` event is fired by the browser to inform Angular of the change (and the need to re-validate). --- The following scenario describes a series of interactions that would run into the problem (on a browser that supports the `date` input type): 1. Initially empty field. - `input.value`: '' - `input.validity`: {valid: true, badInput: false, ...} 2. The user fills part of the value (e.g. the year) using the keyboard. - `input.value`: '' - `input.validity`: {valid: false, badInput: true, ...} - 'input' event: Not fired (since `input.value` hasn't changed) 3. The user fills the value completely (using either the keyboard or the date-picker). - `input.value`: '' - `input.validity`: {valid: true, badInput: false, ...} - 'input' event: Fired 4. The user deletes part of the value (e.g. the year) using the keyboard. - `input.value`: '' (since a partial value is invalid) - `input.validity`: {valid: false, badInput: true, ...} - 'input' event: Fired 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). - `input.value`: '' - `input.validity`: {valid: true, badInput: false, ...} - 'input' event: Not fired (since `input.value` hasn't changed) The problematic cases are (2) and (5), because there is a change in the validity state, but no 'input' event is fired to inform Angular of that change and the need to re-validate. --- This commit fixes the issue by firing an `input` event on `keyup`, correctly triggering re-validation. Fixes #12207 --- src/ng/directive/input.js | 10 + test/ng/directive/inputSpec.js | 352 +++++++++++++++++++++++++++++++++ 2 files changed, 362 insertions(+) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index aa73a5f188cd..11f9b11b732a 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -19,6 +19,7 @@ var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{ var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; +var DATE_INPUT_TYPES = ['date', 'datetime-local', 'month', 'time', 'week']; var inputType = { @@ -1134,6 +1135,15 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // input event on backspace, delete or cut if ($sniffer.hasEvent('input')) { element.on('input', listener); + + // On date-family inputs, we also need to listen for `keyup` in case the date is partially + // edited by the user using the keyboard, resulting in a change in the validity state, but + // without an accompanying change in the input value (thus no `input` event). + // (This is only necessary on browsers that support inputs of that type - other browsers set the + // `type` property to "text".) + var isTypeSupported = (type === attr.type); + var listenForKeyup = isTypeSupported && (DATE_INPUT_TYPES.indexOf(type) !== -1); + if (listenForKeyup) element.on('keyup', function() { element.triggerHandler('input'); }); } else { var timeout; diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 053c64931cfe..fb2b1c77debf 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -645,6 +645,7 @@ describe('input', function() { expect(inputElm.val()).toBe('2013-12'); }); + it('should only change the month of a bound date in any timezone', function() { var inputElm = helper.compileInput(''); @@ -656,6 +657,73 @@ describe('input', function() { expect(inputElm.val()).toBe('2013-09'); }); + + it('should re-validate when partially editing the input value', function() { + // This testcase tests re-validation on interactions involved in the following scenario + // (on a browser that supports this type of input): + // + // 1. Initially empty field. + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // 2. The user fills part of the value (e.g. the year) using the keyboard. + // - `input.value`: '' + // - `input.validity`: {valid: false, badInput: true, ...} + // - 'input' event: Not fired (since `input.value` hasn't changed) + // 3. The user fills the value completely (using either the keyboard or the date-picker). + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // - 'input' event: Fired + // 4. The user deletes part of the value (e.g. the year) using the keyboard. + // - `input.value`: '' (since a partial value is invalid) + // - `input.validity`: {valid: false, badInput: true, ...} + // - 'input' event: Fired + // 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // - 'input' event: Not fired (since `input.value` hasn't changed) + // + // The problematic cases are (2) and (5), because there is a change in the validity state, + // but no 'input' event is fired to inform Angular of that change and the need to re-validate. + + var inputType = 'month'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + browserTrigger(inputElm, 'keyup'); + expect(inputElm).toBeInvalid(); + + mockValidity.valid = true; + mockValidity.badInput = false; + browserTrigger(inputElm, 'keyup'); + expect(inputElm).toBeValid(); + } + }); + + + it('should fire \'input\' event on \'keyup\' only on browsers that support this input type', + function() { + var inputType = 'month'; + + var inputEventExpectedToFire = browserSupportsInputTypeAndEvent(inputType, 'input'); + var inputEventActuallyFired = false; + + var inputElm = helper.compileInput(''); + inputElm.on('input', function() { inputEventActuallyFired = true; }); + + browserTrigger(inputElm, 'keyup'); + + expect(inputEventActuallyFired).toBe(inputEventExpectedToFire); + } + ); + + describe('min', function() { var inputElm; beforeEach(function() { @@ -698,6 +766,7 @@ describe('input', function() { }); }); + describe('max', function() { var inputElm; beforeEach(function() { @@ -870,6 +939,73 @@ describe('input', function() { expect($rootScope.form.alias.$error.week).toBeTruthy(); }); + + it('should re-validate when partially editing the input value', function() { + // This testcase tests re-validation on interactions involved in the following scenario + // (on a browser that supports this type of input): + // + // 1. Initially empty field. + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // 2. The user fills part of the value (e.g. the year) using the keyboard. + // - `input.value`: '' + // - `input.validity`: {valid: false, badInput: true, ...} + // - 'input' event: Not fired (since `input.value` hasn't changed) + // 3. The user fills the value completely (using either the keyboard or the date-picker). + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // - 'input' event: Fired + // 4. The user deletes part of the value (e.g. the year) using the keyboard. + // - `input.value`: '' (since a partial value is invalid) + // - `input.validity`: {valid: false, badInput: true, ...} + // - 'input' event: Fired + // 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // - 'input' event: Not fired (since `input.value` hasn't changed) + // + // The problematic cases are (2) and (5), because there is a change in the validity state, + // but no 'input' event is fired to inform Angular of that change and the need to re-validate. + + var inputType = 'week'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + browserTrigger(inputElm, 'keyup'); + expect(inputElm).toBeInvalid(); + + mockValidity.valid = true; + mockValidity.badInput = false; + browserTrigger(inputElm, 'keyup'); + expect(inputElm).toBeValid(); + } + }); + + + it('should fire \'input\' event on \'keyup\' only on browsers that support this input type', + function() { + var inputType = 'week'; + + var inputEventExpectedToFire = browserSupportsInputTypeAndEvent(inputType, 'input'); + var inputEventActuallyFired = false; + + var inputElm = helper.compileInput(''); + inputElm.on('input', function() { inputEventActuallyFired = true; }); + + browserTrigger(inputElm, 'keyup'); + + expect(inputEventActuallyFired).toBe(inputEventExpectedToFire); + } + ); + + describe('min', function() { var inputElm; beforeEach(function() { @@ -912,6 +1048,7 @@ describe('input', function() { }); }); + describe('max', function() { var inputElm; @@ -1119,6 +1256,73 @@ describe('input', function() { expect($rootScope.form.alias.$error.datetimelocal).toBeTruthy(); }); + + it('should re-validate when partially editing the input value', function() { + // This testcase tests re-validation on interactions involved in the following scenario + // (on a browser that supports this type of input): + // + // 1. Initially empty field. + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // 2. The user fills part of the value (e.g. the year) using the keyboard. + // - `input.value`: '' + // - `input.validity`: {valid: false, badInput: true, ...} + // - 'input' event: Not fired (since `input.value` hasn't changed) + // 3. The user fills the value completely (using either the keyboard or the date-picker). + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // - 'input' event: Fired + // 4. The user deletes part of the value (e.g. the year) using the keyboard. + // - `input.value`: '' (since a partial value is invalid) + // - `input.validity`: {valid: false, badInput: true, ...} + // - 'input' event: Fired + // 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // - 'input' event: Not fired (since `input.value` hasn't changed) + // + // The problematic cases are (2) and (5), because there is a change in the validity state, + // but no 'input' event is fired to inform Angular of that change and the need to re-validate. + + var inputType = 'datetime-local'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + browserTrigger(inputElm, 'keyup'); + expect(inputElm).toBeInvalid(); + + mockValidity.valid = true; + mockValidity.badInput = false; + browserTrigger(inputElm, 'keyup'); + expect(inputElm).toBeValid(); + } + }); + + + it('should fire \'input\' event on \'keyup\' only on browsers that support this input type', + function() { + var inputType = 'datetime-local'; + + var inputEventExpectedToFire = browserSupportsInputTypeAndEvent(inputType, 'input'); + var inputEventActuallyFired = false; + + var inputElm = helper.compileInput(''); + inputElm.on('input', function() { inputEventActuallyFired = true; }); + + browserTrigger(inputElm, 'keyup'); + + expect(inputEventActuallyFired).toBe(inputEventExpectedToFire); + } + ); + + describe('min', function() { var inputElm; beforeEach(function() { @@ -1161,6 +1365,7 @@ describe('input', function() { }); }); + describe('max', function() { var inputElm; beforeEach(function() { @@ -1444,6 +1649,73 @@ describe('input', function() { expect(+$rootScope.value).toBe(+new Date(2013, 2, 3, 1, 2, 0)); }); + + it('should re-validate when partially editing the input value', function() { + // This testcase tests re-validation on interactions involved in the following scenario + // (on a browser that supports this type of input): + // + // 1. Initially empty field. + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // 2. The user fills part of the value (e.g. the hours) using the keyboard. + // - `input.value`: '' + // - `input.validity`: {valid: false, badInput: true, ...} + // - 'input' event: Not fired (since `input.value` hasn't changed) + // 3. The user fills the value completely (using either the keyboard or the buttons). + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // - 'input' event: Fired + // 4. The user deletes part of the value (e.g. the hours) using the keyboard. + // - `input.value`: '' (since a partial value is invalid) + // - `input.validity`: {valid: false, badInput: true, ...} + // - 'input' event: Fired + // 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // - 'input' event: Not fired (since `input.value` hasn't changed) + // + // The problematic cases are (2) and (5), because there is a change in the validity state, + // but no 'input' event is fired to inform Angular of that change and the need to re-validate. + + var inputType = 'time'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + browserTrigger(inputElm, 'keyup'); + expect(inputElm).toBeInvalid(); + + mockValidity.valid = true; + mockValidity.badInput = false; + browserTrigger(inputElm, 'keyup'); + expect(inputElm).toBeValid(); + } + }); + + + it('should fire \'input\' event on \'keyup\' only on browsers that support this input type', + function() { + var inputType = 'time'; + + var inputEventExpectedToFire = browserSupportsInputTypeAndEvent(inputType, 'input'); + var inputEventActuallyFired = false; + + var inputElm = helper.compileInput(''); + inputElm.on('input', function() { inputEventActuallyFired = true; }); + + browserTrigger(inputElm, 'keyup'); + + expect(inputEventActuallyFired).toBe(inputEventExpectedToFire); + } + ); + + describe('min', function() { var inputElm; beforeEach(function() { @@ -1486,6 +1758,7 @@ describe('input', function() { }); }); + describe('max', function() { var inputElm; beforeEach(function() { @@ -1744,6 +2017,73 @@ describe('input', function() { dealoc(formElm); }); + + it('should re-validate when partially editing the input value', function() { + // This testcase tests re-validation on interactions involved in the following scenario + // (on a browser that supports this type of input): + // + // 1. Initially empty field. + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // 2. The user fills part of the value (e.g. the year) using the keyboard. + // - `input.value`: '' + // - `input.validity`: {valid: false, badInput: true, ...} + // - 'input' event: Not fired (since `input.value` hasn't changed) + // 3. The user fills the value completely (using either the keyboard or the date-picker). + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // - 'input' event: Fired + // 4. The user deletes part of the value (e.g. the year) using the keyboard. + // - `input.value`: '' (since a partial value is invalid) + // - `input.validity`: {valid: false, badInput: true, ...} + // - 'input' event: Fired + // 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). + // - `input.value`: '' + // - `input.validity`: {valid: true, badInput: false, ...} + // - 'input' event: Not fired (since `input.value` hasn't changed) + // + // The problematic cases are (2) and (5), because there is a change in the validity state, + // but no 'input' event is fired to inform Angular of that change and the need to re-validate. + + var inputType = 'date'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + browserTrigger(inputElm, 'keyup'); + expect(inputElm).toBeInvalid(); + + mockValidity.valid = true; + mockValidity.badInput = false; + browserTrigger(inputElm, 'keyup'); + expect(inputElm).toBeValid(); + } + }); + + + it('should fire \'input\' event on \'keyup\' only on browsers that support this input type', + function() { + var inputType = 'date'; + + var inputEventExpectedToFire = browserSupportsInputTypeAndEvent(inputType, 'input'); + var inputEventActuallyFired = false; + + var inputElm = helper.compileInput(''); + inputElm.on('input', function() { inputEventActuallyFired = true; }); + + browserTrigger(inputElm, 'keyup'); + + expect(inputEventActuallyFired).toBe(inputEventExpectedToFire); + } + ); + + describe('min', function() { it('should invalidate', function() { @@ -1783,6 +2123,7 @@ describe('input', function() { }); }); + describe('max', function() { it('should invalidate', function() { @@ -2859,4 +3200,15 @@ describe('input', function() { dealoc(inputElm); }); }); + + + // Helpers + function browserSupportsInputTypeAndEvent(inputType, event) { + var input = jqLite(''); + + var supportsType = (inputType === input.prop('type')); + var supportsEvent = $sniffer.hasEvent(event); + + return supportsType && supportsEvent; + } }); From 1728a2f8b409c86e479e6f3fe6c21848620bf20e Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Fri, 25 Sep 2015 12:40:38 +0300 Subject: [PATCH 2/4] fixup: call the listener directly instead of triggering an `input` event --- src/ng/directive/input.js | 6 +- test/ng/directive/inputSpec.js | 105 ++++++++++++++------------------- 2 files changed, 48 insertions(+), 63 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 11f9b11b732a..d8e7f2d53770 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1141,9 +1141,9 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // without an accompanying change in the input value (thus no `input` event). // (This is only necessary on browsers that support inputs of that type - other browsers set the // `type` property to "text".) - var isTypeSupported = (type === attr.type); - var listenForKeyup = isTypeSupported && (DATE_INPUT_TYPES.indexOf(type) !== -1); - if (listenForKeyup) element.on('keyup', function() { element.triggerHandler('input'); }); + var browserSupportsType = (type === attr.type); + var listenForKeyup = browserSupportsType && (DATE_INPUT_TYPES.indexOf(type) !== -1); + if (listenForKeyup) element.on('keyup', function() { listener('input'); }); } else { var timeout; diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index fb2b1c77debf..c88304a89fe2 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -707,21 +707,18 @@ describe('input', function() { }); - it('should fire \'input\' event on \'keyup\' only on browsers that support this input type', - function() { - var inputType = 'month'; - - var inputEventExpectedToFire = browserSupportsInputTypeAndEvent(inputType, 'input'); - var inputEventActuallyFired = false; + it('should listen for \'keyup\' only on browsers that support this input type', function() { + var inputType = 'month'; - var inputElm = helper.compileInput(''); - inputElm.on('input', function() { inputEventActuallyFired = true; }); + var shouldListenForKeyup = browserSupportsInputTypeAndEvent(inputType, 'input'); + var inputElm = helper.compileInput(''); + var ctrl = inputElm.controller('ngModel'); + spyOn(ctrl, '$setViewValue'); - browserTrigger(inputElm, 'keyup'); + inputElm.triggerHandler('keyup'); - expect(inputEventActuallyFired).toBe(inputEventExpectedToFire); - } - ); + expect(ctrl.$setViewValue.callCount).toBe(shouldListenForKeyup ? 1 : 0); + }); describe('min', function() { @@ -989,21 +986,18 @@ describe('input', function() { }); - it('should fire \'input\' event on \'keyup\' only on browsers that support this input type', - function() { - var inputType = 'week'; - - var inputEventExpectedToFire = browserSupportsInputTypeAndEvent(inputType, 'input'); - var inputEventActuallyFired = false; + it('should listen for \'keyup\' only on browsers that support this input type', function() { + var inputType = 'week'; - var inputElm = helper.compileInput(''); - inputElm.on('input', function() { inputEventActuallyFired = true; }); + var shouldListenForKeyup = browserSupportsInputTypeAndEvent(inputType, 'input'); + var inputElm = helper.compileInput(''); + var ctrl = inputElm.controller('ngModel'); + spyOn(ctrl, '$setViewValue'); - browserTrigger(inputElm, 'keyup'); + inputElm.triggerHandler('keyup'); - expect(inputEventActuallyFired).toBe(inputEventExpectedToFire); - } - ); + expect(ctrl.$setViewValue.callCount).toBe(shouldListenForKeyup ? 1 : 0); + }); describe('min', function() { @@ -1306,21 +1300,18 @@ describe('input', function() { }); - it('should fire \'input\' event on \'keyup\' only on browsers that support this input type', - function() { - var inputType = 'datetime-local'; - - var inputEventExpectedToFire = browserSupportsInputTypeAndEvent(inputType, 'input'); - var inputEventActuallyFired = false; + it('should listen for \'keyup\' only on browsers that support this input type', function() { + var inputType = 'datetime-local'; - var inputElm = helper.compileInput(''); - inputElm.on('input', function() { inputEventActuallyFired = true; }); + var shouldListenForKeyup = browserSupportsInputTypeAndEvent(inputType, 'input'); + var inputElm = helper.compileInput(''); + var ctrl = inputElm.controller('ngModel'); + spyOn(ctrl, '$setViewValue'); - browserTrigger(inputElm, 'keyup'); + inputElm.triggerHandler('keyup'); - expect(inputEventActuallyFired).toBe(inputEventExpectedToFire); - } - ); + expect(ctrl.$setViewValue.callCount).toBe(shouldListenForKeyup ? 1 : 0); + }); describe('min', function() { @@ -1699,21 +1690,18 @@ describe('input', function() { }); - it('should fire \'input\' event on \'keyup\' only on browsers that support this input type', - function() { - var inputType = 'time'; - - var inputEventExpectedToFire = browserSupportsInputTypeAndEvent(inputType, 'input'); - var inputEventActuallyFired = false; + it('should listen for \'keyup\' only on browsers that support this input type', function() { + var inputType = 'time'; - var inputElm = helper.compileInput(''); - inputElm.on('input', function() { inputEventActuallyFired = true; }); + var shouldListenForKeyup = browserSupportsInputTypeAndEvent(inputType, 'input'); + var inputElm = helper.compileInput(''); + var ctrl = inputElm.controller('ngModel'); + spyOn(ctrl, '$setViewValue'); - browserTrigger(inputElm, 'keyup'); + inputElm.triggerHandler('keyup'); - expect(inputEventActuallyFired).toBe(inputEventExpectedToFire); - } - ); + expect(ctrl.$setViewValue.callCount).toBe(shouldListenForKeyup ? 1 : 0); + }); describe('min', function() { @@ -2067,21 +2055,18 @@ describe('input', function() { }); - it('should fire \'input\' event on \'keyup\' only on browsers that support this input type', - function() { - var inputType = 'date'; + it('should listen for \'keyup\' only on browsers that support this input type', function() { + var inputType = 'date'; - var inputEventExpectedToFire = browserSupportsInputTypeAndEvent(inputType, 'input'); - var inputEventActuallyFired = false; + var shouldListenForKeyup = browserSupportsInputTypeAndEvent(inputType, 'input'); + var inputElm = helper.compileInput(''); + var ctrl = inputElm.controller('ngModel'); + spyOn(ctrl, '$setViewValue'); - var inputElm = helper.compileInput(''); - inputElm.on('input', function() { inputEventActuallyFired = true; }); + inputElm.triggerHandler('keyup'); - browserTrigger(inputElm, 'keyup'); - - expect(inputEventActuallyFired).toBe(inputEventExpectedToFire); - } - ); + expect(ctrl.$setViewValue.callCount).toBe(shouldListenForKeyup ? 1 : 0); + }); describe('min', function() { From eabfcd25968a99c440dd4a1263dec96b516c6899 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Fri, 25 Sep 2015 15:14:28 +0300 Subject: [PATCH 3/4] fixup: shorten comment in tests and point to the PR for more details --- test/ng/directive/inputSpec.js | 140 ++++----------------------------- 1 file changed, 15 insertions(+), 125 deletions(-) diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index c88304a89fe2..f15afbc29dae 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -659,31 +659,9 @@ describe('input', function() { it('should re-validate when partially editing the input value', function() { - // This testcase tests re-validation on interactions involved in the following scenario - // (on a browser that supports this type of input): - // - // 1. Initially empty field. - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // 2. The user fills part of the value (e.g. the year) using the keyboard. - // - `input.value`: '' - // - `input.validity`: {valid: false, badInput: true, ...} - // - 'input' event: Not fired (since `input.value` hasn't changed) - // 3. The user fills the value completely (using either the keyboard or the date-picker). - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // - 'input' event: Fired - // 4. The user deletes part of the value (e.g. the year) using the keyboard. - // - `input.value`: '' (since a partial value is invalid) - // - `input.validity`: {valid: false, badInput: true, ...} - // - 'input' event: Fired - // 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // - 'input' event: Not fired (since `input.value` hasn't changed) - // - // The problematic cases are (2) and (5), because there is a change in the validity state, - // but no 'input' event is fired to inform Angular of that change and the need to re-validate. + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 var inputType = 'month'; @@ -938,31 +916,9 @@ describe('input', function() { it('should re-validate when partially editing the input value', function() { - // This testcase tests re-validation on interactions involved in the following scenario - // (on a browser that supports this type of input): - // - // 1. Initially empty field. - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // 2. The user fills part of the value (e.g. the year) using the keyboard. - // - `input.value`: '' - // - `input.validity`: {valid: false, badInput: true, ...} - // - 'input' event: Not fired (since `input.value` hasn't changed) - // 3. The user fills the value completely (using either the keyboard or the date-picker). - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // - 'input' event: Fired - // 4. The user deletes part of the value (e.g. the year) using the keyboard. - // - `input.value`: '' (since a partial value is invalid) - // - `input.validity`: {valid: false, badInput: true, ...} - // - 'input' event: Fired - // 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // - 'input' event: Not fired (since `input.value` hasn't changed) - // - // The problematic cases are (2) and (5), because there is a change in the validity state, - // but no 'input' event is fired to inform Angular of that change and the need to re-validate. + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 var inputType = 'week'; @@ -1252,31 +1208,9 @@ describe('input', function() { it('should re-validate when partially editing the input value', function() { - // This testcase tests re-validation on interactions involved in the following scenario - // (on a browser that supports this type of input): - // - // 1. Initially empty field. - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // 2. The user fills part of the value (e.g. the year) using the keyboard. - // - `input.value`: '' - // - `input.validity`: {valid: false, badInput: true, ...} - // - 'input' event: Not fired (since `input.value` hasn't changed) - // 3. The user fills the value completely (using either the keyboard or the date-picker). - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // - 'input' event: Fired - // 4. The user deletes part of the value (e.g. the year) using the keyboard. - // - `input.value`: '' (since a partial value is invalid) - // - `input.validity`: {valid: false, badInput: true, ...} - // - 'input' event: Fired - // 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // - 'input' event: Not fired (since `input.value` hasn't changed) - // - // The problematic cases are (2) and (5), because there is a change in the validity state, - // but no 'input' event is fired to inform Angular of that change and the need to re-validate. + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 var inputType = 'datetime-local'; @@ -1642,31 +1576,9 @@ describe('input', function() { it('should re-validate when partially editing the input value', function() { - // This testcase tests re-validation on interactions involved in the following scenario - // (on a browser that supports this type of input): - // - // 1. Initially empty field. - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // 2. The user fills part of the value (e.g. the hours) using the keyboard. - // - `input.value`: '' - // - `input.validity`: {valid: false, badInput: true, ...} - // - 'input' event: Not fired (since `input.value` hasn't changed) - // 3. The user fills the value completely (using either the keyboard or the buttons). - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // - 'input' event: Fired - // 4. The user deletes part of the value (e.g. the hours) using the keyboard. - // - `input.value`: '' (since a partial value is invalid) - // - `input.validity`: {valid: false, badInput: true, ...} - // - 'input' event: Fired - // 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // - 'input' event: Not fired (since `input.value` hasn't changed) - // - // The problematic cases are (2) and (5), because there is a change in the validity state, - // but no 'input' event is fired to inform Angular of that change and the need to re-validate. + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 var inputType = 'time'; @@ -2007,31 +1919,9 @@ describe('input', function() { it('should re-validate when partially editing the input value', function() { - // This testcase tests re-validation on interactions involved in the following scenario - // (on a browser that supports this type of input): - // - // 1. Initially empty field. - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // 2. The user fills part of the value (e.g. the year) using the keyboard. - // - `input.value`: '' - // - `input.validity`: {valid: false, badInput: true, ...} - // - 'input' event: Not fired (since `input.value` hasn't changed) - // 3. The user fills the value completely (using either the keyboard or the date-picker). - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // - 'input' event: Fired - // 4. The user deletes part of the value (e.g. the year) using the keyboard. - // - `input.value`: '' (since a partial value is invalid) - // - `input.validity`: {valid: false, badInput: true, ...} - // - 'input' event: Fired - // 5. The user deletes all parts of the value using the keyboard (i.e. clears the field). - // - `input.value`: '' - // - `input.validity`: {valid: true, badInput: false, ...} - // - 'input' event: Not fired (since `input.value` hasn't changed) - // - // The problematic cases are (2) and (5), because there is a change in the validity state, - // but no 'input' event is fired to inform Angular of that change and the need to re-validate. + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 var inputType = 'date'; From 795793d50ee50254cc0c8c0adfc22c9bad34322c Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 21 Jan 2016 16:29:47 +0200 Subject: [PATCH 4/4] fixup: only react on keys that can affect the inputs display value --- src/ng/directive/input.js | 53 +++- test/ng/directive/inputSpec.js | 524 ++++++++++++++++++++++++++------- 2 files changed, 471 insertions(+), 106 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index d8e7f2d53770..7c46c2b24477 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -7,6 +7,7 @@ UNTOUCHED_CLASS: false, TOUCHED_CLASS: false, ngModelMinErr: false, + KEYS_PER_DATE_INPUT_TYPE: true */ // Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 @@ -19,7 +20,34 @@ var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{ var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; -var DATE_INPUT_TYPES = ['date', 'datetime-local', 'month', 'time', 'week']; + +var KEYS = { + backspace: 8, + del: 46, + down: 40, + up: 38 +}; +var KEY_RANGES = { + numeric: [48, 57], + numpadNumeric: [96, 105], + alpha: [65, 90] +}; +var DEFAULT_KEYS_FOR_DATE_INPUT_TYPE = [ + KEY_RANGES.numeric, + KEY_RANGES.numpadNumeric, + KEYS.up, + KEYS.down, + KEYS.backspace, + KEYS.del +]; +var KEYS_PER_DATE_INPUT_TYPE = { + date: DEFAULT_KEYS_FOR_DATE_INPUT_TYPE, + 'datetime-local': DEFAULT_KEYS_FOR_DATE_INPUT_TYPE, + month: DEFAULT_KEYS_FOR_DATE_INPUT_TYPE.concat([KEY_RANGES.alpha]), + time: DEFAULT_KEYS_FOR_DATE_INPUT_TYPE, + week: DEFAULT_KEYS_FOR_DATE_INPUT_TYPE +}; +var DATE_INPUT_TYPES = Object.keys(KEYS_PER_DATE_INPUT_TYPE); var inputType = { @@ -1088,6 +1116,24 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { stringBasedInputType(ctrl); } +function createKeyupListener(type, callback) { + // A list containing single keyCodes and keyCode-ranges (in the form [min, max]) + var keys = KEYS_PER_DATE_INPUT_TYPE[type]; + + return function keyupListener(evt) { + // Ignore if a modifier key is also pressed + if (!evt || evt.altKey || evt.ctrlKey || evt.metaKey || evt.shiftKey) return; + + var key = evt.keyCode; + var affectsInput = keys.some(function(keyOrRange) { + return !isArray(keyOrRange) + ? (key === keyOrRange) : (keyOrRange[0] <= key) && (key <= keyOrRange[1]); + }); + + if (affectsInput) callback('input'); + }; +} + function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { var type = lowercase(element[0].type); @@ -1143,7 +1189,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // `type` property to "text".) var browserSupportsType = (type === attr.type); var listenForKeyup = browserSupportsType && (DATE_INPUT_TYPES.indexOf(type) !== -1); - if (listenForKeyup) element.on('keyup', function() { listener('input'); }); + if (listenForKeyup) element.on('keyup', createKeyupListener(type, listener)); } else { var timeout; @@ -1766,6 +1812,3 @@ var ngValueDirective = function() { } }; }; - - - diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index f15afbc29dae..36887a573221 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -1,6 +1,8 @@ 'use strict'; -/* globals getInputCompileHelper: false */ +/* globals getInputCompileHelper: false, + KEYS_PER_DATE_INPUT_TYPE: false + */ describe('input', function() { var helper, $compile, $rootScope, $browser, $sniffer, $timeout, $q; @@ -658,42 +660,100 @@ describe('input', function() { }); - it('should re-validate when partially editing the input value', function() { - // This testcase simulates keyboard interactions that lead to a change in the validity state - // of the element, but no change in its value (thus no 'input' event). - // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 + they('should re-validate when partially editing the input value (keyCode: $prop)', + getKeyCodesForType('month'), + function(keyCode) { + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 - var inputType = 'month'; + var inputType = 'month'; - if (browserSupportsInputTypeAndEvent(inputType, 'input')) { - var mockValidity = {valid: true, badInput: false}; - helper.compileInput('', mockValidity); - var inputElm = helper.inputElm; + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; - expect(inputElm).toBeValid(); + expect(inputElm).toBeValid(); - mockValidity.valid = false; - mockValidity.badInput = true; - browserTrigger(inputElm, 'keyup'); - expect(inputElm).toBeInvalid(); + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeInvalid(); - mockValidity.valid = true; - mockValidity.badInput = false; - browserTrigger(inputElm, 'keyup'); - expect(inputElm).toBeValid(); + mockValidity.valid = true; + mockValidity.badInput = false; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeValid(); + } } - }); + ); + + + they('should not re-validate when a modifier key is pressed (modifier: $prop)', + ['alt', 'ctrl', 'meta', 'shift'], + function(modifier) { + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 + + var inputType = 'month'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + var mockEvt = {type: 'keyup', keyCode: 8}; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler(mockEvt); + expect(inputElm).toBeInvalid(); + + mockEvt[modifier + 'Key'] = true; + + mockValidity.valid = true; + mockValidity.badInput = false; + inputElm.triggerHandler(mockEvt); + expect(inputElm).toBeInvalid(); + } + } + ); + + + they('should not re-validate on keys that can\'t affect the input (keyCode: $prop)', + getIgnoredKeyCodesForType('month'), + function(keyCode) { + var inputType = 'month'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeValid(); + } + } + ); it('should listen for \'keyup\' only on browsers that support this input type', function() { var inputType = 'month'; var shouldListenForKeyup = browserSupportsInputTypeAndEvent(inputType, 'input'); + var keyCode = getKeyCodesForType(inputType)[0]; var inputElm = helper.compileInput(''); var ctrl = inputElm.controller('ngModel'); spyOn(ctrl, '$setViewValue'); - inputElm.triggerHandler('keyup'); + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); expect(ctrl.$setViewValue.callCount).toBe(shouldListenForKeyup ? 1 : 0); }); @@ -915,42 +975,100 @@ describe('input', function() { }); - it('should re-validate when partially editing the input value', function() { - // This testcase simulates keyboard interactions that lead to a change in the validity state - // of the element, but no change in its value (thus no 'input' event). - // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 + they('should re-validate when partially editing the input value (keyCode: $prop)', + getKeyCodesForType('week'), + function(keyCode) { + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 - var inputType = 'week'; + var inputType = 'week'; - if (browserSupportsInputTypeAndEvent(inputType, 'input')) { - var mockValidity = {valid: true, badInput: false}; - helper.compileInput('', mockValidity); - var inputElm = helper.inputElm; + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; - expect(inputElm).toBeValid(); + expect(inputElm).toBeValid(); - mockValidity.valid = false; - mockValidity.badInput = true; - browserTrigger(inputElm, 'keyup'); - expect(inputElm).toBeInvalid(); + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeInvalid(); - mockValidity.valid = true; - mockValidity.badInput = false; - browserTrigger(inputElm, 'keyup'); - expect(inputElm).toBeValid(); + mockValidity.valid = true; + mockValidity.badInput = false; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeValid(); + } } - }); + ); + + + they('should not re-validate when a modifier key is pressed (modifier: $prop)', + ['alt', 'ctrl', 'meta', 'shift'], + function(modifier) { + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 + + var inputType = 'week'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + var mockEvt = {type: 'keyup', keyCode: 8}; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler(mockEvt); + expect(inputElm).toBeInvalid(); + + mockEvt[modifier + 'Key'] = true; + + mockValidity.valid = true; + mockValidity.badInput = false; + inputElm.triggerHandler(mockEvt); + expect(inputElm).toBeInvalid(); + } + } + ); + + + they('should not re-validate on keys that can\'t affect the input (keyCode: $prop)', + getIgnoredKeyCodesForType('week'), + function(keyCode) { + var inputType = 'week'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeValid(); + } + } + ); it('should listen for \'keyup\' only on browsers that support this input type', function() { var inputType = 'week'; var shouldListenForKeyup = browserSupportsInputTypeAndEvent(inputType, 'input'); + var keyCode = getKeyCodesForType(inputType)[0]; var inputElm = helper.compileInput(''); var ctrl = inputElm.controller('ngModel'); spyOn(ctrl, '$setViewValue'); - inputElm.triggerHandler('keyup'); + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); expect(ctrl.$setViewValue.callCount).toBe(shouldListenForKeyup ? 1 : 0); }); @@ -1207,42 +1325,100 @@ describe('input', function() { }); - it('should re-validate when partially editing the input value', function() { - // This testcase simulates keyboard interactions that lead to a change in the validity state - // of the element, but no change in its value (thus no 'input' event). - // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 + they('should re-validate when partially editing the input value (keyCode: $prop)', + getKeyCodesForType('datetime-local'), + function(keyCode) { + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 - var inputType = 'datetime-local'; + var inputType = 'datetime-local'; - if (browserSupportsInputTypeAndEvent(inputType, 'input')) { - var mockValidity = {valid: true, badInput: false}; - helper.compileInput('', mockValidity); - var inputElm = helper.inputElm; + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; - expect(inputElm).toBeValid(); + expect(inputElm).toBeValid(); - mockValidity.valid = false; - mockValidity.badInput = true; - browserTrigger(inputElm, 'keyup'); - expect(inputElm).toBeInvalid(); + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeInvalid(); - mockValidity.valid = true; - mockValidity.badInput = false; - browserTrigger(inputElm, 'keyup'); - expect(inputElm).toBeValid(); + mockValidity.valid = true; + mockValidity.badInput = false; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeValid(); + } } - }); + ); + + + they('should not re-validate when a modifier key is pressed (modifier: $prop)', + ['alt', 'ctrl', 'meta', 'shift'], + function(modifier) { + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 + + var inputType = 'datetime-local'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + var mockEvt = {type: 'keyup', keyCode: 8}; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler(mockEvt); + expect(inputElm).toBeInvalid(); + + mockEvt[modifier + 'Key'] = true; + + mockValidity.valid = true; + mockValidity.badInput = false; + inputElm.triggerHandler(mockEvt); + expect(inputElm).toBeInvalid(); + } + } + ); + + + they('should not re-validate on keys that can\'t affect the input (keyCode: $prop)', + getIgnoredKeyCodesForType('datetime-local'), + function(keyCode) { + var inputType = 'datetime-local'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeValid(); + } + } + ); it('should listen for \'keyup\' only on browsers that support this input type', function() { var inputType = 'datetime-local'; var shouldListenForKeyup = browserSupportsInputTypeAndEvent(inputType, 'input'); + var keyCode = getKeyCodesForType(inputType)[0]; var inputElm = helper.compileInput(''); var ctrl = inputElm.controller('ngModel'); spyOn(ctrl, '$setViewValue'); - inputElm.triggerHandler('keyup'); + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); expect(ctrl.$setViewValue.callCount).toBe(shouldListenForKeyup ? 1 : 0); }); @@ -1575,42 +1751,100 @@ describe('input', function() { }); - it('should re-validate when partially editing the input value', function() { - // This testcase simulates keyboard interactions that lead to a change in the validity state - // of the element, but no change in its value (thus no 'input' event). - // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 + they('should re-validate when partially editing the input value (keyCode: $prop)', + getKeyCodesForType('time'), + function(keyCode) { + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 - var inputType = 'time'; + var inputType = 'time'; - if (browserSupportsInputTypeAndEvent(inputType, 'input')) { - var mockValidity = {valid: true, badInput: false}; - helper.compileInput('', mockValidity); - var inputElm = helper.inputElm; + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; - expect(inputElm).toBeValid(); + expect(inputElm).toBeValid(); - mockValidity.valid = false; - mockValidity.badInput = true; - browserTrigger(inputElm, 'keyup'); - expect(inputElm).toBeInvalid(); + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeInvalid(); - mockValidity.valid = true; - mockValidity.badInput = false; - browserTrigger(inputElm, 'keyup'); - expect(inputElm).toBeValid(); + mockValidity.valid = true; + mockValidity.badInput = false; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeValid(); + } } - }); + ); + + + they('should not re-validate when a modifier key is pressed (modifier: $prop)', + ['alt', 'ctrl', 'meta', 'shift'], + function(modifier) { + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 + + var inputType = 'time'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + var mockEvt = {type: 'keyup', keyCode: 8}; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler(mockEvt); + expect(inputElm).toBeInvalid(); + + mockEvt[modifier + 'Key'] = true; + + mockValidity.valid = true; + mockValidity.badInput = false; + inputElm.triggerHandler(mockEvt); + expect(inputElm).toBeInvalid(); + } + } + ); + + + they('should not re-validate on keys that can\'t affect the input (keyCode: $prop)', + getIgnoredKeyCodesForType('time'), + function(keyCode) { + var inputType = 'time'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeValid(); + } + } + ); it('should listen for \'keyup\' only on browsers that support this input type', function() { var inputType = 'time'; var shouldListenForKeyup = browserSupportsInputTypeAndEvent(inputType, 'input'); + var keyCode = getKeyCodesForType(inputType)[0]; var inputElm = helper.compileInput(''); var ctrl = inputElm.controller('ngModel'); spyOn(ctrl, '$setViewValue'); - inputElm.triggerHandler('keyup'); + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); expect(ctrl.$setViewValue.callCount).toBe(shouldListenForKeyup ? 1 : 0); }); @@ -1918,42 +2152,100 @@ describe('input', function() { }); - it('should re-validate when partially editing the input value', function() { - // This testcase simulates keyboard interactions that lead to a change in the validity state - // of the element, but no change in its value (thus no 'input' event). - // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 + they('should re-validate when partially editing the input value (keyCode: $prop)', + getKeyCodesForType('date'), + function(keyCode) { + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 - var inputType = 'date'; + var inputType = 'date'; - if (browserSupportsInputTypeAndEvent(inputType, 'input')) { - var mockValidity = {valid: true, badInput: false}; - helper.compileInput('', mockValidity); - var inputElm = helper.inputElm; + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; - expect(inputElm).toBeValid(); + expect(inputElm).toBeValid(); - mockValidity.valid = false; - mockValidity.badInput = true; - browserTrigger(inputElm, 'keyup'); - expect(inputElm).toBeInvalid(); + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeInvalid(); - mockValidity.valid = true; - mockValidity.badInput = false; - browserTrigger(inputElm, 'keyup'); - expect(inputElm).toBeValid(); + mockValidity.valid = true; + mockValidity.badInput = false; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeValid(); + } } - }); + ); + + + they('should not re-validate when a modifier key is pressed (modifier: $prop)', + ['alt', 'ctrl', 'meta', 'shift'], + function(modifier) { + // This testcase simulates keyboard interactions that lead to a change in the validity state + // of the element, but no change in its value (thus no 'input' event). + // For a more detailed explanation, see: https://github.com/angular/angular.js/pull/12902 + + var inputType = 'date'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + var mockEvt = {type: 'keyup', keyCode: 8}; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler(mockEvt); + expect(inputElm).toBeInvalid(); + + mockEvt[modifier + 'Key'] = true; + + mockValidity.valid = true; + mockValidity.badInput = false; + inputElm.triggerHandler(mockEvt); + expect(inputElm).toBeInvalid(); + } + } + ); + + + they('should not re-validate on keys that can\'t affect the input (keyCode: $prop)', + getIgnoredKeyCodesForType('date'), + function(keyCode) { + var inputType = 'date'; + + if (browserSupportsInputTypeAndEvent(inputType, 'input')) { + var mockValidity = {valid: true, badInput: false}; + helper.compileInput('', mockValidity); + var inputElm = helper.inputElm; + + expect(inputElm).toBeValid(); + + mockValidity.valid = false; + mockValidity.badInput = true; + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); + expect(inputElm).toBeValid(); + } + } + ); it('should listen for \'keyup\' only on browsers that support this input type', function() { var inputType = 'date'; var shouldListenForKeyup = browserSupportsInputTypeAndEvent(inputType, 'input'); + var keyCode = getKeyCodesForType(inputType)[0]; var inputElm = helper.compileInput(''); var ctrl = inputElm.controller('ngModel'); spyOn(ctrl, '$setViewValue'); - inputElm.triggerHandler('keyup'); + inputElm.triggerHandler({type: 'keyup', keyCode: keyCode}); expect(ctrl.$setViewValue.callCount).toBe(shouldListenForKeyup ? 1 : 0); }); @@ -3086,4 +3378,34 @@ describe('input', function() { return supportsType && supportsEvent; } + + function getKeyCodesForType(type) { + var keyCodes = []; + + (KEYS_PER_DATE_INPUT_TYPE[type] || []).forEach(function(keyOrRange) { + if (isArray(keyOrRange)) { + var min = keyOrRange[0]; + var max = keyOrRange[1]; + + for (var i = min; i <= max; i++) { + keyCodes.push(i); + } + } else { + keyCodes.push(keyOrRange); + } + }); + + return keyCodes; + } + + function getIgnoredKeyCodesForType(type) { + var keyCodes = []; + + var notIgnored = getKeyCodesForType(type); + for (var i = 1; i <= 222; i++) { + if (notIgnored.indexOf(i) === -1) keyCodes.push(i); + } + + return keyCodes; + } });