-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Make Discover field chooser items keyboard accessible. * Make records count link and plus/minus icons tabbable. * Prevent scrolling when you hit spacebar to toggle a field. * Add accessibleClickKeys service and kbnAccessibleClick directive, with tests.
- Loading branch information
Showing
7 changed files
with
299 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
src/ui/public/accessibility/__tests__/kbn_accessible_click.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import angular from 'angular'; | ||
import sinon from 'auto-release-sinon'; | ||
import expect from 'expect.js'; | ||
import ngMock from 'ng_mock'; | ||
import '../kbn_accessible_click'; | ||
import { | ||
ENTER_KEY, | ||
SPACE_KEY, | ||
} from '../accessible_click_keys'; | ||
|
||
describe('kbnAccessibleClick directive', () => { | ||
let $compile; | ||
let $rootScope; | ||
|
||
beforeEach(ngMock.module('kibana')); | ||
|
||
beforeEach(ngMock.inject(function (_$compile_, _$rootScope_) { | ||
$compile = _$compile_; | ||
$rootScope = _$rootScope_; | ||
})); | ||
|
||
describe('throws an error', () => { | ||
it('when the element is a button', () => { | ||
const html = `<button kbn-accessible-click></button>`; | ||
expect(() => { | ||
$compile(html)($rootScope); | ||
}).to.throwError(/kbnAccessibleClick doesn't need to be used on a button./); | ||
}); | ||
|
||
it('when the element is a link with an href', () => { | ||
const html = `<a href="#" kbn-accessible-click></a>`; | ||
expect(() => { | ||
$compile(html)($rootScope); | ||
}).to.throwError(/kbnAccessibleClick doesn't need to be used on a link if it has a href attribute./); | ||
}); | ||
|
||
it(`when the element doesn't have an ng-click`, () => { | ||
const html = `<div kbn-accessible-click></div>`; | ||
expect(() => { | ||
$compile(html)($rootScope); | ||
}).to.throwError(/kbnAccessibleClick requires ng-click to be defined on its element./); | ||
}); | ||
}); | ||
|
||
describe(`doesn't throw an error`, () => { | ||
it('when the element is a link without an href', () => { | ||
const html = `<a ng-click="noop" kbn-accessible-click></a>`; | ||
expect(() => { | ||
$compile(html)($rootScope); | ||
}).not.to.throwError(); | ||
}); | ||
}); | ||
|
||
describe('adds accessibility attributes', () => { | ||
it('tabindex', () => { | ||
const html = `<div ng-click="noop" kbn-accessible-click></div>`; | ||
const element = $compile(html)($rootScope); | ||
expect(element.attr('tabindex')).to.be('0'); | ||
}); | ||
|
||
it('role', () => { | ||
const html = `<div ng-click="noop" kbn-accessible-click></div>`; | ||
const element = $compile(html)($rootScope); | ||
expect(element.attr('role')).to.be('button'); | ||
}); | ||
}); | ||
|
||
describe(`doesn't override pre-existing accessibility attributes`, () => { | ||
it('tabindex', () => { | ||
const html = `<div ng-click="noop" kbn-accessible-click tabindex="1"></div>`; | ||
const element = $compile(html)($rootScope); | ||
expect(element.attr('tabindex')).to.be('1'); | ||
}); | ||
|
||
it('role', () => { | ||
const html = `<div ng-click="noop" kbn-accessible-click role="submit"></div>`; | ||
const element = $compile(html)($rootScope); | ||
expect(element.attr('role')).to.be('submit'); | ||
}); | ||
}); | ||
|
||
describe(`calls ng-click`, () => { | ||
let scope; | ||
let element; | ||
|
||
beforeEach(function () { | ||
scope = $rootScope.$new(); | ||
scope.handleClick = sinon.stub(); | ||
const html = `<div ng-click="handleClick()" kbn-accessible-click></div>`; | ||
element = $compile(html)(scope); | ||
}); | ||
|
||
it(`on ENTER keyup`, () => { | ||
const e = angular.element.Event('keyup'); // eslint-disable-line new-cap | ||
e.keyCode = ENTER_KEY; | ||
element.trigger(e); | ||
sinon.assert.calledOnce(scope.handleClick); | ||
}); | ||
|
||
it(`on SPACE keyup`, () => { | ||
const e = angular.element.Event('keyup'); // eslint-disable-line new-cap | ||
e.keyCode = SPACE_KEY; | ||
element.trigger(e); | ||
sinon.assert.calledOnce(scope.handleClick); | ||
}); | ||
}); | ||
|
||
describe(`doesn't call ng-click when the element being interacted with is a child`, () => { | ||
let scope; | ||
let child; | ||
|
||
beforeEach(function () { | ||
scope = $rootScope.$new(); | ||
scope.handleClick = sinon.stub(); | ||
const html = `<div ng-click="handleClick()" kbn-accessible-click></div>`; | ||
const element = $compile(html)(scope); | ||
child = angular.element(`<button></button>`); | ||
element.append(child); | ||
}); | ||
|
||
it(`on ENTER keyup`, () => { | ||
const e = angular.element.Event('keyup'); // eslint-disable-line new-cap | ||
e.keyCode = ENTER_KEY; | ||
child.trigger(e); | ||
expect(scope.handleClick.callCount).to.be(0); | ||
}); | ||
|
||
it(`on SPACE keyup`, () => { | ||
const e = angular.element.Event('keyup'); // eslint-disable-line new-cap | ||
e.keyCode = SPACE_KEY; | ||
child.trigger(e); | ||
expect(scope.handleClick.callCount).to.be(0); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export const ENTER_KEY = 13; | ||
export const SPACE_KEY = 32; | ||
|
||
// These keys are used to execute click actions on interactive elements like buttons and links. | ||
export const accessibleClickKeys = { | ||
[ENTER_KEY]: 'enter', | ||
[SPACE_KEY]: 'space', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/** | ||
* Interactive elements must be able to receive focus. | ||
* | ||
* Ideally, this means using elements that are natively keyboard accessible (<a href="">, | ||
* <input type="button">, or <button>). Note that links should be used when navigating and buttons | ||
* should be used when performing an action on the page. | ||
* | ||
* If you need to use a <div>, <p>, or <a> without the href attribute, then you need to allow | ||
* them to receive focus and to respond to keyboard input. The workaround is to: | ||
* | ||
* - Give the element tabindex="0" so that it can receive keyboard focus. | ||
* - Add a JavaScript onkeyup event handler that triggers element functionality if the Enter key | ||
* is pressed while the element is focused. This is necessary because some browsers do not trigger | ||
* onclick events for such elements when activated via the keyboard. | ||
* - If the item is meant to function as a button, the onkeyup event handler should also detect the | ||
* Spacebar in addition to the Enter key, and the element should be given role="button". | ||
* | ||
* Apply this directive to any of these elements to automatically do the above. | ||
*/ | ||
|
||
import { | ||
accessibleClickKeys, | ||
SPACE_KEY, | ||
} from './accessible_click_keys'; | ||
import { uiModules } from 'ui/modules'; | ||
|
||
uiModules.get('kibana') | ||
.directive('kbnAccessibleClick', function () { | ||
return { | ||
restrict: 'A', | ||
controller: $element => { | ||
$element.on('keydown', e => { | ||
// If the user is interacting with a different element, then we don't need to do anything. | ||
if (e.currentTarget !== e.target) { | ||
return; | ||
} | ||
|
||
// Prevent a scroll from occurring if the user has hit space. | ||
if (e.keyCode === SPACE_KEY) { | ||
e.preventDefault(); | ||
} | ||
}); | ||
}, | ||
link: (scope, element, attrs) => { | ||
// The whole point of this directive is to hack in functionality that native buttons provide | ||
// by default. | ||
const elementType = element.prop('tagName'); | ||
|
||
if (elementType === 'BUTTON') { | ||
throw new Error(`kbnAccessibleClick doesn't need to be used on a button.`); | ||
} | ||
|
||
if (elementType === 'A' && attrs.href !== undefined) { | ||
throw new Error(`kbnAccessibleClick doesn't need to be used on a link if it has a href attribute.`); | ||
} | ||
|
||
// We're emulating a click action, so we should already have a regular click handler defined. | ||
if (!attrs.ngClick) { | ||
throw new Error('kbnAccessibleClick requires ng-click to be defined on its element.'); | ||
} | ||
|
||
// If the developer hasn't already specified attributes required for accessibility, add them. | ||
if (attrs.tabindex === undefined) { | ||
element.attr('tabindex', '0'); | ||
} | ||
|
||
if (attrs.role === undefined) { | ||
element.attr('role', 'button'); | ||
} | ||
|
||
element.on('keyup', e => { | ||
// If the user is interacting with a different element, then we don't need to do anything. | ||
if (e.currentTarget !== e.target) { | ||
return; | ||
} | ||
|
||
// Support keyboard accessibility by emulating mouse click on ENTER or SPACE keypress. | ||
if (accessibleClickKeys[e.keyCode]) { | ||
// Delegate to the click handler on the element (assumed to be ng-click). | ||
element.click(); | ||
} | ||
}); | ||
}, | ||
}; | ||
}); |