From 251cfed82837270c29e1099b34f5e85874a7bcad Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Wed, 20 Feb 2019 20:01:46 -0500 Subject: [PATCH] fix(checkbox): enter submits form when submit button is disabled (#11640) match the behavior of the native input type checkbox Fixes #11639 --- src/components/checkbox/checkbox.js | 18 +- src/components/checkbox/checkbox.spec.js | 305 ++++++++++++++++++++++- src/core/util/util.js | 3 +- 3 files changed, 312 insertions(+), 14 deletions(-) diff --git a/src/components/checkbox/checkbox.js b/src/components/checkbox/checkbox.js index 2be6943d540..6f803c061dd 100644 --- a/src/components/checkbox/checkbox.js +++ b/src/components/checkbox/checkbox.js @@ -169,8 +169,13 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ } } + /** + * @param {KeyboardEvent} ev 'keypress' event to handle + */ function keypressHandler(ev) { var keyCode = ev.which || ev.keyCode; + var submit, form; + ev.preventDefault(); switch (keyCode) { case $mdConstant.KEY_CODE.SPACE: @@ -178,10 +183,17 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ listener(ev); break; case $mdConstant.KEY_CODE.ENTER: - var form = $mdUtil.getClosest(ev.target, 'form'); - // We have to use a native event here as the form directive does not use jqlite + // Match the behavior of the native . + // When the enter key is pressed while focusing a native checkbox inside a form, + // the browser will trigger a `click` on the first non-disabled submit button/input + // in the form. Note that this is different from text inputs, which + // will directly submit the form without needing a submit button/input to be present. + form = $mdUtil.getClosest(ev.target, 'form'); if (form) { - form.dispatchEvent(new Event('submit')); + submit = form.querySelector('button[type="submit"]:enabled, input[type="submit"]:enabled'); + if (submit) { + submit.click(); + } } break; } diff --git a/src/components/checkbox/checkbox.spec.js b/src/components/checkbox/checkbox.spec.js index f010fe6d4dc..2cc1d2548ae 100644 --- a/src/components/checkbox/checkbox.spec.js +++ b/src/components/checkbox/checkbox.spec.js @@ -2,7 +2,7 @@ describe('mdCheckbox', function() { var CHECKED_CSS = 'md-checked'; var INDETERMINATE_CSS = 'md-indeterminate'; - var $compile, $log, pageScope, $mdConstant; + var $compile, $log, pageScope, $mdConstant, $window; beforeEach(module('ngAria', 'material.components.checkbox')); @@ -10,9 +10,10 @@ describe('mdCheckbox', function() { $compile = $injector.get('$compile'); $log = $injector.get('$log'); $mdConstant = $injector.get('$mdConstant'); + $window = $injector.get('$window'); var $rootScope = $injector.get('$rootScope'); - pageScope = $rootScope.$new(); + pageScope = $rootScope.$new(false); })); function compileAndLink(template, opt_scope) { @@ -298,15 +299,299 @@ describe('mdCheckbox', function() { expect(isChecked(checkbox)).toBe(false); }); - it('should submit a parent form when ENTER is pressed', function () { - var form = compileAndLink('
'); - angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ - type: 'keypress', - keyCode: $mdConstant.KEY_CODE.ENTER + it('should not submit a parent form when ENTER is pressed and there is no submit button/input', + function() { + var submitted = false; + pageScope.onSubmit = function() { + submitted = true; + }; + var form = compileAndLink('
' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(false); + expect(pageScope.form.$submitted).toBe(false); + $window.document.body.removeChild(node); + }); + + it('should click an enabled submit button when ENTER is pressed', + function() { + var submitted = false; + pageScope.onSubmit = function() { + submitted = true; + }; + var form = compileAndLink( + '
' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(true); + expect(pageScope.form.$submitted).toBe(true); + $window.document.body.removeChild(node); + }); + + it('should click an enabled submit input when ENTER is pressed', + function() { + var submitted = false; + pageScope.onSubmit = function() { + submitted = true; + }; + var form = compileAndLink( + '
' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(true); + expect(pageScope.form.$submitted).toBe(true); + $window.document.body.removeChild(node); + }); + + it('should submit a parent form when ENTER is pressed and there is an enabled submit button', + function() { + var submitted = false; + pageScope.onSubmit = function() { + submitted = true; + }; + var form = compileAndLink( + '
' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(true); + expect(pageScope.form.$submitted).toBe(true); + $window.document.body.removeChild(node); + }); + + it('should submit a parent form when ENTER is pressed and there is an enabled submit input', + function() { + var submitted = false; + pageScope.onSubmit = function() { + submitted = true; + }; + var form = compileAndLink( + '
' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(true); + expect(pageScope.form.$submitted).toBe(true); + $window.document.body.removeChild(node); + }); + + it('should not submit a parent form when ENTER is pressed and the submit components are disabled', + function() { + var submitted = false; + pageScope.onSubmit = function() { + submitted = true; + }; + var form = compileAndLink( + '
' + + '' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(false); + expect(pageScope.form.$submitted).toBe(false); + $window.document.body.removeChild(node); + }); + + it('should click the first enabled submit input when there are multiple', + function() { + var submitted = false, submitted2 = false; + pageScope.onSubmit = function() { + submitted = true; + }; + pageScope.onSubmit2 = function() { + submitted2 = true; + }; + var form = compileAndLink( + '
' + + '' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(true); + expect(submitted2).toBe(false); + expect(pageScope.form.$submitted).toBe(true); + $window.document.body.removeChild(node); + }); + + it('should click the first enabled submit button when there are multiple', + function() { + var submitted = false, submitted2 = false; + pageScope.onSubmit = function() { + submitted = true; + }; + pageScope.onSubmit2 = function() { + submitted2 = true; + }; + var form = compileAndLink( + '
' + + '' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(true); + expect(submitted2).toBe(false); + expect(pageScope.form.$submitted).toBe(true); + $window.document.body.removeChild(node); + }); + + it('should click the first submit button when there is a submit input after it', + function() { + var submitted = false, submitted2 = false; + pageScope.onSubmit = function() { + submitted = true; + }; + pageScope.onSubmit2 = function() { + submitted2 = true; + }; + var form = compileAndLink( + '
' + + '' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(true); + expect(submitted2).toBe(false); + expect(pageScope.form.$submitted).toBe(true); + $window.document.body.removeChild(node); + }); + + it('should click the first submit input when there is a submit button after it', + function() { + var submitted = false, submitted2 = false; + pageScope.onSubmit = function() { + submitted = true; + }; + pageScope.onSubmit2 = function() { + submitted2 = true; + }; + var form = compileAndLink( + '
' + + '' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(true); + expect(submitted2).toBe(false); + expect(pageScope.form.$submitted).toBe(true); + $window.document.body.removeChild(node); + }); + + it('should click the submit button when the first submit input is disabled', + function() { + var submitted = false, submitted2 = false; + pageScope.onSubmit = function() { + submitted = true; + }; + pageScope.onSubmit2 = function() { + submitted2 = true; + }; + var form = compileAndLink( + '
' + + '' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(false); + expect(submitted2).toBe(true); + expect(pageScope.form.$submitted).toBe(true); + $window.document.body.removeChild(node); + }); + + it('should click the submit input when the first submit button is disabled', + function() { + var submitted = false, submitted2 = false; + pageScope.onSubmit = function() { + submitted = true; + }; + pageScope.onSubmit2 = function() { + submitted2 = true; + }; + var form = compileAndLink( + '
' + + '' + + '
'); + + // We need to add the form to the DOM in order for `submit` events to be properly fired. + var node = $window.document.body.appendChild(form[0]); + angular.element(form[0].getElementsByTagName('md-checkbox')[0]).triggerHandler({ + type: 'keypress', + keyCode: $mdConstant.KEY_CODE.ENTER + }); + pageScope.$apply(); + expect(submitted).toBe(false); + expect(submitted2).toBe(true); + expect(pageScope.form.$submitted).toBe(true); + $window.document.body.removeChild(node); }); - pageScope.$apply(); - expect(form[0].classList.contains('ng-submitted')).toBe(true); - }); it('should mark the checkbox as selected on load with ng-checked', function() { pageScope.isChecked = function() { return true; }; diff --git a/src/core/util/util.js b/src/core/util/util.js index 70a036d35e6..804ab41b0fc 100644 --- a/src/core/util/util.js +++ b/src/core/util/util.js @@ -537,7 +537,8 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in * @param {string|function} validateWith If a string is passed, it will be evaluated against * each of the parent nodes' tag name. If a function is passed, the loop will call it with each * of the parents and will use the return value to determine whether the node is a match. - * @param {boolean} onlyParent Only start checking from the parent element, not `el`. + * @param {boolean=} onlyParent Only start checking from the parent element, not `el`. + * @returns {Node|null} closest matching parent Node or null if not found */ getClosest: function getClosest(el, validateWith, onlyParent) { if (angular.isString(validateWith)) {