mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Move htmlIdGenerator to ui_framework
This commit is contained in:
parent
e6f453dbd9
commit
98e88ac017
5 changed files with 336 additions and 0 deletions
283
style_guides/accessibility_guide.md
Normal file
283
style_guides/accessibility_guide.md
Normal file
|
@ -0,0 +1,283 @@
|
|||
# Accessibility (A11Y) Guide
|
||||
|
||||
This document provides some technical guidelines how to prevent several common
|
||||
accessibility issues.
|
||||
|
||||
## Naming elements
|
||||
|
||||
### `aria-label` and `aria-labelledby`
|
||||
|
||||
Every element on a page will have a name, that is read out to an assistive technology
|
||||
like a screen reader. This will for most elements be the content of the element.
|
||||
For form elements it will be the content of the assosciated `<label>` (see below).
|
||||
|
||||
You can overwrite that name, that is read out, by specifying a new name via the
|
||||
`aria-label` attribute. This must e.g. be done, if the element itself has no
|
||||
visual text representation (e.g. an icon button):
|
||||
|
||||
```html
|
||||
<button aria-label="Add filter"><span class="fa fa-plus"></span></button>
|
||||
```
|
||||
|
||||
If the actual name for that element is already present in another element,
|
||||
you can use `aria-labelledby` to reference the id of that element:
|
||||
|
||||
```html
|
||||
<div id="datepicker">Date Picker</div>
|
||||
<button aria-labelledby="datepicker"><span class="fa fa-calendar"></span></button>
|
||||
```
|
||||
|
||||
### Label every form element
|
||||
|
||||
You should add a label for every form element:
|
||||
|
||||
```html
|
||||
<label for="interval">Interval</label>
|
||||
<select id="interval"><!-- ... --></select>
|
||||
```
|
||||
|
||||
If one label references multiple form elements, you can use the reverse logic
|
||||
and add `aria-labelledby` to all form elements:
|
||||
|
||||
```html
|
||||
<label id="fromLabel">From</label>
|
||||
<input type="number" aria-labelledby="fromLabel">
|
||||
<input type="number" aria-labelledby="fromLabel">
|
||||
<input type="number" aria-labelledby="fromLabel">
|
||||
```
|
||||
|
||||
You should always prefer the `<label for>` solution, since it also adds benefit
|
||||
for every user, by making the label clickable, to directly jump to the form
|
||||
element (or in case of checkboxes and radio buttons directly check them).
|
||||
|
||||
#### How to generate ids?
|
||||
|
||||
When labeling elements (and for some other accessibility tasks) you will often need
|
||||
ids. Ids must be unique within the page i.e. no duplicate ids in the rendered DOM
|
||||
at any time.
|
||||
|
||||
Since we have some components that are used multiple times on the page, you must
|
||||
make sure every instance of that component has a unique `id`. To make the generation
|
||||
of those `id`s easier, you can use the `htmlIdGenerator` service in the `ui_framework/services`.
|
||||
|
||||
A react component could use it as follows:
|
||||
|
||||
```jsx
|
||||
import { htmlIdGenerator } from 'ui_framework/services';
|
||||
|
||||
render() {
|
||||
// Create a new generator that will create ids deterministic
|
||||
const htmlId = htmlIdGenerator();
|
||||
return (<div>
|
||||
<label htmlFor={htmlId('agg')}>Aggregation</label>
|
||||
<input id={htmlId('agg')}/>
|
||||
</div>);
|
||||
}
|
||||
```
|
||||
|
||||
Each id generator you create by calling `htmlIdGenerator()` will generate unique but
|
||||
deterministic ids. As you can see in the above example, that single generator
|
||||
created the same id in the label's `htmlFor` as well as the input's `id`.
|
||||
|
||||
A single generator instance will create the same id when passed the same argument
|
||||
to the function multiple times. But two different generators will produce two different
|
||||
ids for the same argument to the function, as you can see in the following example:
|
||||
|
||||
```js
|
||||
const generatorOne = htmlIdGenerator();
|
||||
const generatorTwo = htmlIdGenerator();
|
||||
|
||||
// Those statements are always true:
|
||||
// Same generator
|
||||
generatorOne('foo') === generatorOne('foo');
|
||||
generatorOne('foo') !== generatorOne('bar');
|
||||
|
||||
// Different generator
|
||||
generatorOne('foo') !== generatorTwo('foo')
|
||||
```
|
||||
|
||||
This allows multiple instances of a single react component to now have different ids.
|
||||
If you include the above react component multiple times in the same page,
|
||||
each component instance will have a unique id, because each render method will use a different
|
||||
id generator.
|
||||
|
||||
You can use this service of course also outside of react.
|
||||
|
||||
### Don't use the `title` attribute
|
||||
|
||||
**TL;DR** *Don't use the `title` attribute, use tooltips, `aria-label`, etc. instead.*
|
||||
|
||||
The `title` has no clear role within the accessibility standards.
|
||||
[See the HTML5 spec](http://w3c.github.io/html/dom.html#the-title-attribute) for more information.
|
||||
|
||||
To provide supplementary, descriptive information about a form control, button, link, or other element,
|
||||
that should also be visible to non vision impaired users, use a tooltip component instead.
|
||||
|
||||
If you need a label only for screen readers use `aria-label`.
|
||||
|
||||
**Additional reading:**
|
||||
|
||||
* https://www.paciellogroup.com/blog/2010/11/using-the-html-title-attribute/
|
||||
* https://www.paciellogroup.com/blog/2012/01/html5-accessibility-chops-title-attribute-use-and-abuse/
|
||||
* https://www.deque.com/blog/text-links-practices-screen-readers/
|
||||
|
||||
## Interactive elements
|
||||
|
||||
### Use `<button>` and `<a href>`
|
||||
|
||||
**TL;DR** *Use `<button>` and `<a>` (with `href`) instead of click listeners on other elements
|
||||
and style it whatever way you need.*
|
||||
|
||||
If you want to make an element clickable, use a `<button>` or `<a href>` element for it.
|
||||
Use a `<button>` whenever it causes an action on the current page, and an `<a href>` if it
|
||||
navigates to a different page. You can use click listeners just fine on these elements.
|
||||
|
||||
An `<a>` element must have an `href` attribute, so that (a) it will be correctly perceived
|
||||
as a link by a screen reader and (b) that registered click listener will correctly
|
||||
trigger on pressing <kbd>Enter</kbd>. If you don't need it, make it `href="#"`
|
||||
and call `preventDefault()` on the click event.
|
||||
|
||||
*Why not use other elements?*
|
||||
|
||||
If you create e.g. a `<div>` with a click listener (like `<div ng-click="ctrl.doSomething()">...</div>`)
|
||||
you will create multiple accessibility issues:
|
||||
|
||||
* The element is not *keyboard accessible*, meaning:
|
||||
* You cannot focus it by pressing <kbd>tab</kbd>. You can add this behavior by
|
||||
adding `tabindex="0"` to the element.
|
||||
* You cannot trigger the click by pressing <kbd>Enter</kbd> or <kbd>Space</kbd>.
|
||||
We have a `kbn-accessible-click` directive for AngularJS and a `KuiKeyboardAccessible`
|
||||
React component to add that behavior.
|
||||
* Even if you make it keyboard accessible, a user using a screen reader won't
|
||||
recognize, that the div is actually an interactive element, the screen reader
|
||||
will just announce something like: "Sort". You would need
|
||||
to add `role="button"` or `role="link"` to it, so that the screen reader would
|
||||
actually announce something like "Sort, button" and give the user the required
|
||||
information, that this element is interactive.
|
||||
|
||||
You will need quite some work just to rebuild native logic of a button (and we haven't
|
||||
even touched disabled states, etc.). It is most of the time easier to just use
|
||||
a `button` or an `a` and style this, whatever way you want it to look (even if
|
||||
you don't want it to look like a button at all).
|
||||
|
||||
### tabindex and id
|
||||
|
||||
**TL;DR** *Only use `tabindex="0"` and `tabindex="-1"` and no values above 0. Always
|
||||
add an `id` to an element with `tabindex`.*
|
||||
|
||||
If you want to make an element focusable, that doesn't offer this feature by default
|
||||
(i.e. isn't an form element or a link), you can add `tabindex` with a value >= 0 to it.
|
||||
|
||||
*Why shouldn't you use values above 0?*
|
||||
|
||||
Setting `tabindex="0"` will add the element to the tabbing order at the position
|
||||
it is in the DOM. Setting it to something above 0 will create an explicit order.
|
||||
Tabbing will always focus all elements with an tabindex > 0, starting from the smallest
|
||||
number. After all those elements has been tabbed, it will continue with all `tabindex="0"`
|
||||
or implicit tabable elements. These values are not scoped to a subtree of the
|
||||
DOM, but are global values. Maintaining a global order is nearly impossible
|
||||
and considered a [serious issue](https://dequeuniversity.com/rules/axe/1.1/tabindex)
|
||||
by automated accessibility testing frameworks.
|
||||
|
||||
`tabindex="-1"` will remove an element from tab order, that would be focusable
|
||||
otherwise. You won't need this often.
|
||||
|
||||
*Why should you add an `id`?*
|
||||
|
||||
Due to some bugs in some common screen readers, you will always need to add an `id`
|
||||
to an element with `tabindex`, since they wouldn't pick up the `tabindex` correctly
|
||||
otherwise.
|
||||
|
||||
### Tooltips
|
||||
|
||||
**TL;DR** *Add `role="tooltip"` and `aria-describedby` to elements for accessible tooltips.*
|
||||
|
||||
Elements which act as tooltips should have the `role="tooltip"` attribute and an ID to which the
|
||||
described element can point to with the `aria-describedby` attribute. For example:
|
||||
|
||||
```html
|
||||
<div
|
||||
class="kuiTooltip"
|
||||
role="tooltip"
|
||||
id="visualizationsTooltip"
|
||||
>
|
||||
Visualizations help you make sense of your data.
|
||||
</div>
|
||||
|
||||
<button aria-describedby="visualizationsTooltip">
|
||||
Visualizations
|
||||
</button>
|
||||
```
|
||||
|
||||
### Don't create keyboard traps
|
||||
|
||||
**TL;DR** *If you can't leave an element with <kbd>Tab</kbd> again, it needs a special interaction model.*
|
||||
|
||||
If an interactive element consumes the <kbd>Tab</kbd> key (e.g. a code editor to
|
||||
create an actual tabular indentation) it will prevent a keyboard user to leave
|
||||
that element again. Also see [WCAG 2.1.2](https://www.w3.org/TR/WCAG20/#keyboard-operation-trapping).
|
||||
|
||||
Those kind of elements, require a special interaction model. A [code editor](https://github.com/elastic/kibana/pull/13339)
|
||||
could require an <kbd>Enter</kbd> keypress before starting editing mode, and
|
||||
could leave that mode on <kbd>Escape</kbd> again.
|
||||
|
||||
Unfortunately there is no universal solution for this problem, so be aware when creating
|
||||
such elements, that would consume tabbing, to think about an accessible interaction
|
||||
model.
|
||||
|
||||
*Hint:* If you create that kind of interactive elements `role="application"` might
|
||||
be a good `role` (also see below) for that element. It is meant for elements providing
|
||||
their own interaction schemes.
|
||||
|
||||
## Roles
|
||||
|
||||
Each DOM element has an implicit role in the accessibility tree (that assistive technologies
|
||||
use). The mapping of elements to default roles can be found in the
|
||||
[Accessibility API Mappings](https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings).
|
||||
You can overwrite this role via the `role` attribute on an element, and the
|
||||
assistive technology will now behave the same like any other element with that role
|
||||
(e.g. behave like it is a button when it has `role="button"`).
|
||||
|
||||
### Landmark roles
|
||||
|
||||
Some roles can be used to declare so called landmarks. These landmarks tag important
|
||||
parts of a web page. Screen readers offer a quick way to jump to these
|
||||
parts of the page (*landmark navigation*).
|
||||
|
||||
#### role=main
|
||||
|
||||
The `main` role (or equivalent using the `<main>` tag) declares the main part
|
||||
of a page. This can be used in the landmark navigation to quickly jump to the
|
||||
actual main area of the page (and skip all headers, navigations, etc.).
|
||||
|
||||
#### `<section>`
|
||||
|
||||
The `<section>` element, can be used to mark a region on the page, so that it
|
||||
appears in the landmark navigation. The section element therefore needs to have
|
||||
an *accessible name*, i.e. you should add an `aria-label`, that gives a short
|
||||
title to that section of the page.
|
||||
|
||||
### role=search
|
||||
|
||||
**TL;DR** *Place `role="search"` neither on the `<input>` nor the `<form>`, but
|
||||
some `div` in between.*
|
||||
|
||||
Role search can be used to mark a region as used for searching. This can be used
|
||||
by assistive technologies to quickly find and navigate to this section.
|
||||
|
||||
If you place it on the `input` you will overwrite the implicit `textbox` or `searchbox`
|
||||
role, and as such confuse the user, since it loses it meaning as in input element.
|
||||
If you place it on the `form` element you will also overwrite its role and
|
||||
remove it from a quick jump navigation to all forms.
|
||||
|
||||
That's why it should be placed to an `div` (or any other container) between the
|
||||
`form` and the `input`. In most cases we already have a div there that you can
|
||||
easily put this role to.
|
||||
|
||||
**Related Links:**
|
||||
|
||||
* [Where to put your search role?](http://adrianroselli.com/2015/08/where-to-put-your-search-role.html)
|
||||
* Discussions about making `search` role inherit the `form` role:
|
||||
[wcag/113](https://github.com/w3c/wcag/issues/113),
|
||||
[html-aria/118](https://github.com/w3c/html-aria/issues/18),
|
||||
[aria/85](https://github.com/w3c/aria/issues/85)
|
12
ui_framework/services/accessibility/html_id_generator.js
Normal file
12
ui_framework/services/accessibility/html_id_generator.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import uuid from 'uuid';
|
||||
|
||||
/**
|
||||
* This function returns a function to generate ids.
|
||||
* This can be used to generate unique, but predictable ids to pair labels
|
||||
* with their inputs. It takes an optional prefix as a parameter. If you don't
|
||||
* specify it, it generates a random id prefix.
|
||||
*/
|
||||
export function htmlIdGenerator(idPrefix) {
|
||||
const prefix = idPrefix || uuid.v1();
|
||||
return (suffix) => `${prefix}_${suffix || uuid.v1()}`;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { htmlIdGenerator } from './html_id_generator';
|
||||
|
||||
describe('htmlIdGenerator', () => {
|
||||
|
||||
it('should return a function', () => {
|
||||
const fn = htmlIdGenerator();
|
||||
expect(typeof fn).toBe('function');
|
||||
});
|
||||
|
||||
it('should return an id ending with the specified suffix', () => {
|
||||
expect(htmlIdGenerator()('suf')).toMatch(/suf$/);
|
||||
});
|
||||
|
||||
it('should return an id beginning with the specified prefix', () => {
|
||||
expect(htmlIdGenerator('pref')('foo')).toMatch(/^pref/);
|
||||
});
|
||||
|
||||
it('should create the same id for the same suffix', () => {
|
||||
const idGenerator = htmlIdGenerator();
|
||||
expect(idGenerator('foo')).toBe(idGenerator('foo'));
|
||||
});
|
||||
|
||||
it('should create different ids for different suffixes', () => {
|
||||
const idGenerator = htmlIdGenerator();
|
||||
expect(idGenerator('foo')).not.toBe(idGenerator('bar'));
|
||||
});
|
||||
|
||||
it('should generate different ids on different instances', () => {
|
||||
const idGenerator1 = htmlIdGenerator();
|
||||
const idGenerator2 = htmlIdGenerator();
|
||||
expect(idGenerator1('foo')).not.toBe(idGenerator2('foo'));
|
||||
});
|
||||
|
||||
it('should generate different ids if no suffix is passed', () => {
|
||||
const generator = htmlIdGenerator();
|
||||
expect(generator()).not.toBe(generator());
|
||||
});
|
||||
|
||||
});
|
|
@ -5,3 +5,4 @@ export {
|
|||
} from './accessible_click_keys';
|
||||
|
||||
export { comboBoxKeyCodes } from './combo_box_key_codes';
|
||||
export { htmlIdGenerator } from './html_id_generator';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export {
|
||||
accessibleClickKeys,
|
||||
comboBoxKeyCodes,
|
||||
htmlIdGenerator,
|
||||
ENTER_KEY,
|
||||
SPACE_KEY,
|
||||
} from './accessibility';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue