Skip to content

Commit

Permalink
feat(a11y): add label for screen readers (#1121)
Browse files Browse the repository at this point in the history
Fixes #1096 

BREAKING CHANGE: `description` prop in `<Settings/>` is renamed to `ariaDescription`

Co-authored-by: Marco Vettorello <vettorello.marco@gmail.com>
  • Loading branch information
rshen91 and markov00 authored Apr 22, 2021
1 parent ab82bde commit 920e585
Show file tree
Hide file tree
Showing 13 changed files with 371 additions and 123 deletions.
10 changes: 7 additions & 3 deletions api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ export const DEFAULT_TOOLTIP_SNAP = true;
export const DEFAULT_TOOLTIP_TYPE: "vertical";

// @public (undocumented)
export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'debug' | 'tooltip' | 'theme' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' | 'externalPointerEvents' | 'showLegend' | 'showLegendExtra' | 'legendPosition' | 'legendMaxDepth' | 'description' | 'useDefaultSummary';
export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'debug' | 'tooltip' | 'theme' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' | 'externalPointerEvents' | 'showLegend' | 'showLegendExtra' | 'legendPosition' | 'legendMaxDepth' | 'ariaUseDefaultSummary' | 'ariaLabelHeadingLevel';

// @public (undocumented)
export const DEPTH_KEY = "depth";
Expand Down Expand Up @@ -1738,12 +1738,17 @@ export interface SettingsSpec extends Spec, LegendSpec {
allowBrushingLastHistogramBucket?: boolean;
// (undocumented)
animateData: boolean;
ariaDescribedBy?: string;
ariaDescription?: string;
ariaLabel?: string;
ariaLabelHeadingLevel: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
ariaLabelledBy?: string;
ariaUseDefaultSummary: boolean;
baseTheme?: Theme;
brushAxis?: BrushAxis;
debug: boolean;
// @alpha
debugState?: boolean;
description?: string;
// @alpha
externalPointerEvents: ExternalPointerEventsSettings;
hideDuplicateAxes: boolean;
Expand Down Expand Up @@ -1774,7 +1779,6 @@ export interface SettingsSpec extends Spec, LegendSpec {
roundHistogramBrushValues?: boolean;
theme?: PartialTheme | PartialTheme[];
tooltip: TooltipSettings;
useDefaultSummary: boolean;
// (undocumented)
xDomain?: CustomXDomain;
}
Expand Down
52 changes: 24 additions & 28 deletions src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';

import { LegendItem } from '../../../../common/legend';
import { Description } from '../../../../components/accessibility/description';
import { Label } from '../../../../components/accessibility/label';
import { onChartRendered } from '../../../../state/actions/chart';
import { GlobalChartState } from '../../../../state/chart_state';
import {
A11ySettings,
DEFAULT_A11_SETTINGS,
getA11ySettingsSelector,
} from '../../../../state/selectors/get_accessibility_config';
import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions';
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation';
import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme';
import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized';
Expand Down Expand Up @@ -79,9 +85,7 @@ export interface ReactiveChartStateProps {
annotationSpecs: AnnotationSpec[];
panelGeoms: PanelGeoms;
seriesTypes: Set<SeriesType>;
description?: string;
useDefaultSummary: boolean;
chartId: string;
a11ySettings: A11ySettings;
}

interface ReactiveChartDispatchProps {
Expand Down Expand Up @@ -159,9 +163,7 @@ class XYChartComponent extends React.Component<XYChartProps> {
isChartEmpty,
chartContainerDimensions: { width, height },
seriesTypes,
description,
useDefaultSummary,
chartId,
a11ySettings,
} = this.props;

if (!initialized || isChartEmpty) {
Expand All @@ -171,9 +173,9 @@ class XYChartComponent extends React.Component<XYChartProps> {

const chartSeriesTypes =
seriesTypes.size > 1 ? `Mixed chart: ${[...seriesTypes].join(' and ')} chart` : `${[...seriesTypes]} chart`;
const chartIdDescription = `${chartId}--description`;

return (
<figure>
<figure aria-labelledby={a11ySettings.labelId} aria-describedby={a11ySettings.descriptionId}>
<canvas
ref={forwardStageRef}
className="echCanvasRenderer"
Expand All @@ -185,19 +187,17 @@ class XYChartComponent extends React.Component<XYChartProps> {
}}
// eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role
role="presentation"
{...(description ? { 'aria-describedby': chartIdDescription } : {})}
>
{(description || useDefaultSummary) && (
<div className="echScreenReaderOnly">
{description && <p id={chartIdDescription}>{description}</p>}
{useDefaultSummary && (
<dl>
<dt>Chart type</dt>
<dd>{chartSeriesTypes}</dd>
</dl>
)}
</div>
)}
<div className="echScreenReaderOnly">
<Label {...a11ySettings} />
<Description {...a11ySettings} />
{a11ySettings.defaultSummaryId && (
<dl id={a11ySettings.defaultSummaryId}>
<dt>Chart type</dt>
<dd>{chartSeriesTypes}</dd>
</dl>
)}
</div>
</canvas>
</figure>
);
Expand Down Expand Up @@ -252,9 +252,7 @@ const DEFAULT_PROPS: ReactiveChartStateProps = {
annotationSpecs: [],
panelGeoms: [],
seriesTypes: new Set(),
description: undefined,
useDefaultSummary: true,
chartId: '',
a11ySettings: DEFAULT_A11_SETTINGS,
};

const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
Expand All @@ -263,7 +261,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
}

const { geometries, geometriesIndex } = computeSeriesGeometriesSelector(state);
const { debug, description, useDefaultSummary } = getSettingsSpecSelector(state);
const { debug } = getSettingsSpecSelector(state);

return {
initialized: true,
Expand All @@ -285,9 +283,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
annotationSpecs: getAnnotationSpecsSelector(state),
panelGeoms: computePanelsSelectors(state),
seriesTypes: getSeriesTypes(state),
description,
useDefaultSummary,
chartId: getChartIdSelector(state),
a11ySettings: getA11ySettingsSelector(state),
};
};

Expand Down
76 changes: 0 additions & 76 deletions src/chart_types/xy_chart/state/chart_state.a11y.test.ts

This file was deleted.

161 changes: 161 additions & 0 deletions src/chart_types/xy_chart/state/chart_state.accessibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { Store } from 'redux';

import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs';
import { MockStore } from '../../../mocks/store/store';
import { GlobalChartState } from '../../../state/chart_state';
import { DEFAULT_A11_SETTINGS } from '../../../state/selectors/get_accessibility_config';
import { getSettingsSpecSelector } from '../../../state/selectors/get_settings_specs';

describe('test accessibility prop defaults', () => {
let store: Store<GlobalChartState>;
beforeEach(() => {
store = MockStore.default();
MockStore.addSpecs(
[
MockSeriesSpec.bar({
data: [
{ x: 1, y: 10 },
{ x: 2, y: 5 },
],
}),
MockGlobalSpec.settings(),
],
store,
);
});
it('should test defaults', () => {
const state = store.getState();
const {
ariaDescription,
ariaUseDefaultSummary,
ariaLabelHeadingLevel,
ariaLabel,
ariaLabelledBy,
} = getSettingsSpecSelector(state);
expect(ariaDescription).toBeUndefined();
expect(ariaUseDefaultSummary).toBeTrue();
expect(ariaLabelHeadingLevel).toBe(DEFAULT_A11_SETTINGS.labelHeadingLevel);
expect(ariaLabel).toBeUndefined();
expect(ariaLabelledBy).toBeUndefined();
});
});
describe('custom description for screen readers', () => {
let store: Store<GlobalChartState>;
beforeEach(() => {
store = MockStore.default();
MockStore.addSpecs(
[
MockSeriesSpec.bar({
data: [
{ x: 1, y: 10 },
{ x: 2, y: 5 },
],
}),
MockGlobalSpec.settings(),
],
store,
);
});
it('should allow user to set a custom description for chart', () => {
MockStore.addSpecs(
[
MockGlobalSpec.settings({
ariaDescription: 'This is sample Kibana data',
}),
],
store,
);
const state = store.getState();
const { ariaDescription } = getSettingsSpecSelector(state);
expect(ariaDescription).toBe('This is sample Kibana data');
});
it('should be able to disable generated descriptions', () => {
MockStore.addSpecs(
[
MockGlobalSpec.settings({
ariaUseDefaultSummary: false,
}),
],
store,
);
const state = store.getState();
const { ariaUseDefaultSummary } = getSettingsSpecSelector(state);
expect(ariaUseDefaultSummary).toBe(false);
});
});
describe('custom labels for screen readers', () => {
let store: Store<GlobalChartState>;
beforeEach(() => {
store = MockStore.default();
MockStore.addSpecs(
[
MockSeriesSpec.bar({
data: [
{ x: 1, y: 10 },
{ x: 2, y: 5 },
],
}),
MockGlobalSpec.settings(),
],
store,
);
});
it('should allow label set by the user', () => {
MockStore.addSpecs(
[
MockGlobalSpec.settings({
ariaLabel: 'Label set by user',
}),
],
store,
);
const state = store.getState();
const { ariaLabel } = getSettingsSpecSelector(state);
expect(ariaLabel).toBe('Label set by user');
});
it('should allow labelledBy set by the user', () => {
MockStore.addSpecs(
[
MockGlobalSpec.settings({
ariaLabelledBy: 'label-id',
}),
],
store,
);
const state = store.getState();
const { ariaLabelledBy } = getSettingsSpecSelector(state);
expect(ariaLabelledBy).toBe('label-id');
});
it('should allow users to specify valid heading levels', () => {
MockStore.addSpecs(
[
MockGlobalSpec.settings({
ariaLabelHeadingLevel: 'h5',
}),
],
store,
);
const state = store.getState();
const { ariaLabelHeadingLevel } = getSettingsSpecSelector(state);
expect(ariaLabelHeadingLevel).toBe('h5');
});
});
Loading

0 comments on commit 920e585

Please sign in to comment.