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:
Lee Adcock
2016-02-17 03:57:58 +00:00
committed by Georgios Kalpakas
parent 30436957ed
commit ad41baa1fd
2 changed files with 80 additions and 44 deletions
+23 -20
View File
@@ -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
View File
@@ -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();
});
});