Move htmlIdGenerator to ui_framework (#13906)

* Move htmlIdGenerator to ui_framework

* Use jest expect instead of chai

* Add htmlIdGenerator to accessibility styleguide

* Improve the writing of the accessibility styleguide
This commit is contained in:
Tim Roes 2017-09-11 13:22:32 +02:00 committed by GitHub
parent 0315b983e1
commit 7cc823c863
29 changed files with 87 additions and 33 deletions

View file

@ -10,7 +10,7 @@ import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import Vars from './vars';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
class CalculationAgg extends Component {

View file

@ -6,7 +6,7 @@ import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
export const DerivativeAgg = props => {
const { siblings } = props;

View file

@ -6,7 +6,7 @@ import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
export const FilterRatioAgg = props => {
const {

View file

@ -8,7 +8,7 @@ import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import createNumberHandler from '../lib/create_number_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
export const MovingAverageAgg = props => {
const { siblings } = props;

View file

@ -10,7 +10,7 @@ import Select from 'react-select';
import uuid from 'uuid';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
const newPercentile = (opts) => {
return _.assign({ id: uuid.v1(), mode: 'line', shade: 0.2 }, opts);
};

View file

@ -6,7 +6,7 @@ import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
export const PercentileRankAgg = props => {
const { series, panel, fields } = props;

View file

@ -6,7 +6,7 @@ import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createNumberHandler from '../lib/create_number_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
export const SerialDiffAgg = props => {
const { siblings } = props;

View file

@ -5,7 +5,7 @@ import Select from 'react-select';
import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
function SeriesAgg(props) {
const { model } = props;

View file

@ -5,7 +5,7 @@ import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
export const Static = props => {
const handleChange = createChangeHandler(props.onChange, props.model);

View file

@ -5,7 +5,7 @@ import FieldSelect from './field_select';
import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
function StandardAgg(props) {
const { model, panel, series, fields } = props;

View file

@ -7,7 +7,7 @@ import Select from 'react-select';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
export const StandardDeviationAgg = props => {
const { series, panel, fields } = props;

View file

@ -7,7 +7,7 @@ import Select from 'react-select';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
export const StandardSiblingAgg = props => {
const { siblings } = props;

View file

@ -8,7 +8,7 @@ import FieldSelect from './aggs/field_select';
import uuid from 'uuid';
import IconSelect from './icon_select';
import YesNo from './yes_no';
import { htmlIdGenerator } from '../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
function newAnnotation() {
return {

View file

@ -5,7 +5,7 @@ import AddDeleteButtons from './add_delete_buttons';
import Select from 'react-select';
import * as collectionActions from './lib/collection_actions';
import ColorPicker from './color_picker';
import { htmlIdGenerator } from '../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
class ColorRules extends Component {

View file

@ -4,7 +4,7 @@ import FieldSelect from './aggs/field_select';
import createSelectHandler from './lib/create_select_handler';
import createTextHandler from './lib/create_text_handler';
import YesNo from './yes_no';
import { htmlIdGenerator } from '../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
export const IndexPattern = props => {
const { fields, prefix } = props;

View file

@ -9,7 +9,7 @@ import ColorRules from '../color_rules';
import ColorPicker from '../color_picker';
import uuid from 'uuid';
import YesNo from 'plugins/metrics/components/yes_no';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
class GaugePanelConfig extends Component {

View file

@ -11,7 +11,7 @@ import ColorPicker from '../color_picker';
import YesNo from '../yes_no';
import MarkdownEditor from '../markdown_editor';
import less from 'less/lib/less-browser';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
const lessC = less(window, { env: 'production' });
class MarkdownPanelConfig extends Component {

View file

@ -6,7 +6,7 @@ import createTextHandler from '../lib/create_text_handler';
import ColorRules from '../color_rules';
import YesNo from '../yes_no';
import uuid from 'uuid';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
class MetricPanelConfig extends Component {

View file

@ -8,7 +8,7 @@ import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import ColorPicker from '../color_picker';
import YesNo from '../yes_no';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
class TimeseriesPanelConfig extends Component {

View file

@ -7,7 +7,7 @@ import ColorRules from '../color_rules';
import ColorPicker from '../color_picker';
import uuid from 'uuid';
import YesNo from '../yes_no';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
class TopNPanelConfig extends Component {

View file

@ -5,7 +5,7 @@ import createSelectHandler from './lib/create_select_handler';
import createTextHandler from './lib/create_text_handler';
import YesNo from './yes_no';
import { IndexPattern } from './index_pattern';
import { htmlIdGenerator } from '../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
export const SeriesConfig = props => {
const defaults = { offset_time: '', value_template: '' };

View file

@ -6,7 +6,7 @@ import createSelectHandler from '../../lib/create_select_handler';
import YesNo from '../../yes_no';
import createTextHandler from '../../lib/create_text_handler';
import { IndexPattern } from '../../index_pattern';
import { htmlIdGenerator } from '../../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
function TimeseriesConfig(props) {
const handleSelectChange = createSelectHandler(props.onChange);

View file

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import createLegendSeries from '../lib/create_legend_series';
import reactcss from 'reactcss';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
function HorizontalLegend(props) {
const rows = props.series.map(createLegendSeries(props));

View file

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import createLegendSeries from '../lib/create_legend_series';
import reactcss from 'reactcss';
import { htmlIdGenerator } from '../../lib/html_id_generator';
import { htmlIdGenerator } from 'ui_framework/services';
function VerticalLegend(props) {
const rows = props.series.map(createLegendSeries(props));

View file

@ -50,6 +50,59 @@ 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.*

View file

@ -1,40 +1,39 @@
import { expect } from 'chai';
import { htmlIdGenerator } from '../html_id_generator';
import { htmlIdGenerator } from './html_id_generator';
describe('htmlIdGenerator', () => {
it('should return a function', () => {
const fn = htmlIdGenerator();
expect(fn).to.be.a('function');
expect(typeof fn).toBe('function');
});
it('should return an id ending with the specified suffix', () => {
expect(htmlIdGenerator()('suf')).to.match(/suf$/);
expect(htmlIdGenerator()('suf')).toMatch(/suf$/);
});
it('should return an id beginning with the specified prefix', () => {
expect(htmlIdGenerator('pref')('foo')).to.match(/^pref/);
expect(htmlIdGenerator('pref')('foo')).toMatch(/^pref/);
});
it('should create the same id for the same suffix', () => {
const idGenerator = htmlIdGenerator();
expect(idGenerator('foo')).to.equal(idGenerator('foo'));
expect(idGenerator('foo')).toBe(idGenerator('foo'));
});
it('should create different ids for different suffixes', () => {
const idGenerator = htmlIdGenerator();
expect(idGenerator('foo')).not.to.equal(idGenerator('bar'));
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.to.equal(idGenerator2('foo'));
expect(idGenerator1('foo')).not.toBe(idGenerator2('foo'));
});
it('should generate different ids if no suffix is passed', () => {
const generator = htmlIdGenerator();
expect(generator()).not.to.equal(generator());
expect(generator()).not.toBe(generator());
});
});

View file

@ -1,2 +1,3 @@
export { accessibleClickKeys } from './accessible_click_keys';
export { comboBoxKeyCodes } from './combo_box_key_codes';
export { htmlIdGenerator } from './html_id_generator';

View file

@ -5,6 +5,7 @@ export { keyCodes };
export {
accessibleClickKeys,
comboBoxKeyCodes,
htmlIdGenerator
} from './accessibility';
export { SortableProperties } from './sort';