fix(ngAria): bind to keydown instead of keypress in ngClick
Previously, `ngAria` would provide keyboard support for non-native buttons (via `ngClick`), by binding the `ngClick` handler to the `keypress` event. In an attempt to better emulate the behavior of native buttons, `ngAria` will now bind to the `keydown` event (instead of `keypress`). The configuration flag for this feature has been renamed from `bindKeypress` to `bindKeydown`, to closer describe the underlying behavior. Fixes #14063 Closes #14065 BREAKING CHANGE: If you were explicitly setting the value of the `bindKeypress` flag, you need to change your code to use `bindKeydown` instead. Before: `$ariaProvider.config({bindKeypress: xyz})` After: `$ariaProvider.config({bindKeydown: xyz})` **Note:** If the element already has any of the `ngKeydown`/`ngKeyup`/`ngKeypress` directives, `ngAria` will _not_ bind to the `keydown` event, since it assumes that the developer has already taken care of keyboard interaction for that element. Although it is not expected to affect many applications, it might be desirable to keep the previous behavior of binding to the `keypress` event instead of the `keydown`. In that case, you need to manually use the `ngKeypress` directive (in addition to `ngClick`). Before: ```html <div ng-click="onClick()"> I respond to `click` and `keypress` (not `keydown`) </div> ``` After: ```html <div ng-click="onClick()" ng-keypress="onClick()"> I respond to `click` and `keypress` (not `keydown`) </div> <!-- OR --> <div ng-click="onClick()"> I respond to `click` and `keydown` (not `keypress`) </div> ``` Finally, it is possible that this change affects your unit or end-to-end tests. If you are currently expecting your custom buttons to automatically respond to the `keypress` event (due to `ngAria`), you need to change the tests to trigger `keydown` events instead.
This commit is contained in:
committed by
Georgios Kalpakas
parent
30436957ed
commit
ad41baa1fd
+23
-20
@@ -21,19 +21,19 @@
|
||||
*
|
||||
* Below is a more detailed breakdown of the attributes handled by ngAria:
|
||||
*
|
||||
* | Directive | Supported Attributes |
|
||||
* |---------------------------------------------|----------------------------------------------------------------------------------------|
|
||||
* | Directive | Supported Attributes |
|
||||
* |---------------------------------------------|-----------------------------------------------------------------------------------------------------|
|
||||
* | {@link ng.directive:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required, input roles |
|
||||
* | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled |
|
||||
* | {@link ng.directive:ngRequired ngRequired} | aria-required
|
||||
* | {@link ng.directive:ngChecked ngChecked} | aria-checked
|
||||
* | {@link ng.directive:ngReadonly ngReadonly} | aria-readonly |
|
||||
* | {@link ng.directive:ngValue ngValue} | aria-checked |
|
||||
* | {@link ng.directive:ngShow ngShow} | aria-hidden |
|
||||
* | {@link ng.directive:ngHide ngHide} | aria-hidden |
|
||||
* | {@link ng.directive:ngDblclick ngDblclick} | tabindex |
|
||||
* | {@link module:ngMessages ngMessages} | aria-live |
|
||||
* | {@link ng.directive:ngClick ngClick} | tabindex, keypress event, button role |
|
||||
* | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled |
|
||||
* | {@link ng.directive:ngRequired ngRequired} | aria-required |
|
||||
* | {@link ng.directive:ngChecked ngChecked} | aria-checked |
|
||||
* | {@link ng.directive:ngReadonly ngReadonly} | aria-readonly |
|
||||
* | {@link ng.directive:ngValue ngValue} | aria-checked |
|
||||
* | {@link ng.directive:ngShow ngShow} | aria-hidden |
|
||||
* | {@link ng.directive:ngHide ngHide} | aria-hidden |
|
||||
* | {@link ng.directive:ngDblclick ngDblclick} | tabindex |
|
||||
* | {@link module:ngMessages ngMessages} | aria-live |
|
||||
* | {@link ng.directive:ngClick ngClick} | tabindex, keydown event, button role |
|
||||
*
|
||||
* Find out more information about each directive by reading the
|
||||
* {@link guide/accessibility ngAria Developer Guide}.
|
||||
@@ -98,7 +98,7 @@ function $AriaProvider() {
|
||||
ariaInvalid: true,
|
||||
ariaValue: true,
|
||||
tabindex: true,
|
||||
bindKeypress: true,
|
||||
bindKeydown: true,
|
||||
bindRoleForClick: true
|
||||
};
|
||||
|
||||
@@ -114,12 +114,15 @@ function $AriaProvider() {
|
||||
* - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags
|
||||
* - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags
|
||||
* - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags
|
||||
* - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and aria-valuenow tags
|
||||
* - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and
|
||||
* aria-valuenow tags
|
||||
* - **tabindex** – `{boolean}` – Enables/disables tabindex tags
|
||||
* - **bindKeypress** – `{boolean}` – Enables/disables keypress event binding on `div` and
|
||||
* `li` elements with ng-click
|
||||
* - **bindRoleForClick** – `{boolean}` – Adds role=button to non-interactive elements like `div`
|
||||
* using ng-click, making them more accessible to users of assistive technologies
|
||||
* - **bindKeydown** – `{boolean}` – Enables/disables keyboard event binding on non-interactive
|
||||
* elements (such as `div` or `li`) using ng-click, making them more accessible to users of
|
||||
* assistive technologies
|
||||
* - **bindRoleForClick** – `{boolean}` – Adds role=button to non-interactive elements (such as
|
||||
* `div` or `li`) using ng-click, making them more accessible to users of assistive
|
||||
* technologies
|
||||
*
|
||||
* @description
|
||||
* Enables/disables various ARIA attributes
|
||||
@@ -373,8 +376,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
|
||||
elem.attr('tabindex', 0);
|
||||
}
|
||||
|
||||
if ($aria.config('bindKeypress') && !attr.ngKeypress) {
|
||||
elem.on('keypress', function(event) {
|
||||
if ($aria.config('bindKeydown') && !attr.ngKeydown && !attr.ngKeypress && !attr.ngKeyup) {
|
||||
elem.on('keydown', function(event) {
|
||||
var keyCode = event.which || event.keyCode;
|
||||
if (keyCode === 32 || keyCode === 13) {
|
||||
scope.$apply(callback);
|
||||
|
||||
+57
-24
@@ -681,8 +681,8 @@ describe('$aria', function() {
|
||||
var divElement = elements.find('div');
|
||||
var liElement = elements.find('li');
|
||||
|
||||
divElement.triggerHandler({type: 'keypress', keyCode: 32});
|
||||
liElement.triggerHandler({type: 'keypress', keyCode: 32});
|
||||
divElement.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
liElement.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
|
||||
expect(clickFn).toHaveBeenCalledWith('div');
|
||||
expect(clickFn).toHaveBeenCalledWith('li');
|
||||
@@ -703,32 +703,57 @@ describe('$aria', function() {
|
||||
var divElement = elements.find('div');
|
||||
var liElement = elements.find('li');
|
||||
|
||||
divElement.triggerHandler({type: 'keypress', which: 32});
|
||||
liElement.triggerHandler({type: 'keypress', which: 32});
|
||||
divElement.triggerHandler({type: 'keydown', which: 32});
|
||||
liElement.triggerHandler({type: 'keydown', which: 32});
|
||||
|
||||
expect(clickFn).toHaveBeenCalledWith('div');
|
||||
expect(clickFn).toHaveBeenCalledWith('li');
|
||||
});
|
||||
|
||||
it('should not override existing ng-keypress', function() {
|
||||
scope.someOtherAction = function() {};
|
||||
var keypressFn = spyOn(scope, 'someOtherAction');
|
||||
it('should not bind to key events if there is existing ng-keydown', function() {
|
||||
scope.onClick = jasmine.createSpy('onClick');
|
||||
scope.onKeydown = jasmine.createSpy('onKeydown');
|
||||
|
||||
scope.someAction = function() {};
|
||||
clickFn = spyOn(scope, 'someAction');
|
||||
compileElement('<div ng-click="someAction()" ng-keypress="someOtherAction()" tabindex="0"></div>');
|
||||
var tmpl = '<div ng-click="onClick()" ng-keydown="onKeydown()" tabindex="0"></div>';
|
||||
compileElement(tmpl);
|
||||
|
||||
element.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
|
||||
expect(scope.onKeydown).toHaveBeenCalled();
|
||||
expect(scope.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not bind to key events if there is existing ng-keypress', function() {
|
||||
scope.onClick = jasmine.createSpy('onClick');
|
||||
scope.onKeypress = jasmine.createSpy('onKeypress');
|
||||
|
||||
var tmpl = '<div ng-click="onClick()" ng-keypress="onKeypress()" tabindex="0"></div>';
|
||||
compileElement(tmpl);
|
||||
|
||||
element.triggerHandler({type: 'keypress', keyCode: 32});
|
||||
|
||||
expect(clickFn).not.toHaveBeenCalled();
|
||||
expect(keypressFn).toHaveBeenCalled();
|
||||
expect(scope.onKeypress).toHaveBeenCalled();
|
||||
expect(scope.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update bindings when keypress handled', function() {
|
||||
it('should not bind to key events if there is existing ng-keyup', function() {
|
||||
scope.onClick = jasmine.createSpy('onClick');
|
||||
scope.onKeyup = jasmine.createSpy('onKeyup');
|
||||
|
||||
var tmpl = '<div ng-click="onClick()" ng-keyup="onKeyup()" tabindex="0"></div>';
|
||||
compileElement(tmpl);
|
||||
|
||||
element.triggerHandler({type: 'keyup', keyCode: 32});
|
||||
|
||||
expect(scope.onKeyup).toHaveBeenCalled();
|
||||
expect(scope.onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update bindings when keydown is handled', function() {
|
||||
compileElement('<div ng-click="text = \'clicked!\'">{{text}}</div>');
|
||||
expect(element.text()).toBe('');
|
||||
spyOn(scope.$root, '$digest').and.callThrough();
|
||||
element.triggerHandler({ type: 'keypress', keyCode: 13 });
|
||||
element.triggerHandler({ type: 'keydown', keyCode: 13 });
|
||||
expect(element.text()).toBe('clicked!');
|
||||
expect(scope.$root.$digest).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -737,14 +762,14 @@ describe('$aria', function() {
|
||||
compileElement('<div ng-click="event = $event">{{event.type}}' +
|
||||
'{{event.keyCode}}</div>');
|
||||
expect(element.text()).toBe('');
|
||||
element.triggerHandler({ type: 'keypress', keyCode: 13 });
|
||||
expect(element.text()).toBe('keypress13');
|
||||
element.triggerHandler({ type: 'keydown', keyCode: 13 });
|
||||
expect(element.text()).toBe('keydown13');
|
||||
});
|
||||
|
||||
it('should not bind keypress to elements not in the default config', function() {
|
||||
it('should not bind keydown to natively interactive elements', function() {
|
||||
compileElement('<button ng-click="event = $event">{{event.type}}{{event.keyCode}}</button>');
|
||||
expect(element.text()).toBe('');
|
||||
element.triggerHandler({ type: 'keypress', keyCode: 13 });
|
||||
element.triggerHandler({ type: 'keydown', keyCode: 13 });
|
||||
expect(element.text()).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -761,21 +786,29 @@ describe('$aria', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions when bindKeypress is set to false', function() {
|
||||
describe('actions when bindKeydown is set to false', function() {
|
||||
beforeEach(configAriaProvider({
|
||||
bindKeypress: false
|
||||
bindKeydown: false
|
||||
}));
|
||||
beforeEach(injectScopeAndCompiler);
|
||||
|
||||
it('should not a trigger click', function() {
|
||||
scope.someAction = function() {};
|
||||
var clickFn = spyOn(scope, 'someAction');
|
||||
it('should not trigger click', function() {
|
||||
scope.someAction = jasmine.createSpy('someAction');
|
||||
|
||||
element = $compile('<div ng-click="someAction()" tabindex="0"></div>')(scope);
|
||||
|
||||
element.triggerHandler({type: 'keydown', keyCode: 13});
|
||||
element.triggerHandler({type: 'keydown', keyCode: 32});
|
||||
element.triggerHandler({type: 'keypress', keyCode: 13});
|
||||
element.triggerHandler({type: 'keypress', keyCode: 32});
|
||||
element.triggerHandler({type: 'keyup', keyCode: 13});
|
||||
element.triggerHandler({type: 'keyup', keyCode: 32});
|
||||
|
||||
expect(clickFn).not.toHaveBeenCalled();
|
||||
expect(scope.someAction).not.toHaveBeenCalled();
|
||||
|
||||
element.triggerHandler({type: 'click', keyCode: 32});
|
||||
|
||||
expect(scope.someAction).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user