Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(a11y): allow user to pass custom description for screen readers #1111

Merged
merged 12 commits into from
Apr 14, 2021
41 changes: 9 additions & 32 deletions .playground/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,22 @@

import React from 'react';

import { Chart, AreaSeries, LineSeries, BarSeries, ScaleType } from '../src';
import { Chart, AreaSeries, ScaleType, Settings } from '../src';
import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana';

export class Playground extends React.Component {
render() {
return (
<div className="App">
<Chart size={[500, 200]}>
<Settings customDescription="This is an area chart showing kibana sample data." />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Settings customDescription="This is an area chart showing kibana sample data." />
<Settings description="This is an area chart showing kibana sample data." />

<AreaSeries
id="lines"
name="test2"
data={[
{ x: 'trousers', y: 300, val: 1232 },
{ x: 'watches', y: 20, val: 1232 },
{ x: 'bags', y: 700, val: 1232 },
{ x: 'cocktail dresses', y: 804, val: 1232 },
]}
/>
<LineSeries
id="lines2"
name="test"
data={[
{ x: 'trousers', y: 300, val: 1232 },
{ x: 'watches', y: 20, val: 1232 },
{ x: 'bags', y: 700, val: 1232 },
{ x: 'cocktail dresses', y: 804, val: 1232 },
]}
/>
<BarSeries
id="bars"
name="amount"
xScaleType={ScaleType.Ordinal}
xAccessor="x"
yAccessors={['y']}
data={[
{ x: 'trousers', y: 390, val: 1222 },
{ x: 'watches', y: 23, val: 1222 },
{ x: 'bags', y: 750, val: 1222 },
{ x: 'cocktail dresses', y: 854, val: 1222 },
]}
id="area"
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={0}
yAccessors={[1]}
data={KIBANA_METRICS.metrics.kibana_os_load[0].data}
/>
</Chart>
</div>
Expand Down
4 changes: 3 additions & 1 deletion api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,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';
export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'debug' | 'tooltip' | 'theme' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' | 'externalPointerEvents' | 'showLegend' | 'showLegendExtra' | 'legendPosition' | 'legendMaxDepth' | 'description' | 'useDefaultSummary';

// @public (undocumented)
export const DEPTH_KEY = "depth";
Expand Down Expand Up @@ -1739,6 +1739,7 @@ export interface SettingsSpec extends Spec, LegendSpec {
debug: boolean;
// @alpha
debugState?: boolean;
description?: string;
// @alpha
externalPointerEvents: ExternalPointerEventsSettings;
hideDuplicateAxes: boolean;
Expand Down Expand Up @@ -1769,6 +1770,7 @@ export interface SettingsSpec extends Spec, LegendSpec {
roundHistogramBrushValues?: boolean;
theme?: PartialTheme | PartialTheme[];
tooltip: TooltipSettings;
useDefaultSummary: boolean;
// (undocumented)
xDomain?: CustomXDomain;
}
Expand Down
10 changes: 10 additions & 0 deletions integration/page_objects/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,16 @@ class CommonPage {
});
return accessibilitySnapshot;
}

/**
* Get HTML for element to test aria labels etc
*/
// eslint-disable-next-line class-methods-use-this
async getElementHTML(url: string) {
await this.loadElementFromURL(url);
// https://github.com/puppeteer/puppeteer/issues/406#issuecomment-323555639
return await page.evaluate(() => new XMLSerializer().serializeToString(document));
}
}

export const common = new CommonPage();
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 25 additions & 5 deletions src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { LegendItem } from '../../../../common/legend';
import { onChartRendered } from '../../../../state/actions/chart';
import { GlobalChartState } from '../../../../state/chart_state';
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 @@ -78,6 +79,9 @@ export interface ReactiveChartStateProps {
annotationSpecs: AnnotationSpec[];
panelGeoms: PanelGeoms;
seriesTypes: Set<SeriesType>;
description?: string;
useDefaultSummary: boolean;
chartId: string;
}

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

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

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

return (
<figure>
<canvas
Expand All @@ -178,11 +184,19 @@ class XYChartComponent extends React.Component<XYChartProps> {
}}
// eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role
role="presentation"
{...(description ? { 'aria-describedby': `${chartId}--${chartSeriesTypes.length}` } : {})}
>
<dl className="echScreen-reader">
<dt>Chart type</dt>
<dd>{chartSeriesTypes}</dd>
</dl>
{(description || useDefaultSummary) && (
<div className="echScreenReaderOnly">
{description && <p id={`${chartId}--${chartSeriesTypes.length}`}>{description}</p>}
markov00 marked this conversation as resolved.
Show resolved Hide resolved
{useDefaultSummary && (
<dl>
<dt>Chart type</dt>
<dd>{chartSeriesTypes}</dd>
</dl>
)}
</div>
)}
</canvas>
</figure>
);
Expand Down Expand Up @@ -237,6 +251,9 @@ const DEFAULT_PROPS: ReactiveChartStateProps = {
annotationSpecs: [],
panelGeoms: [],
seriesTypes: new Set(),
description: undefined,
useDefaultSummary: true,
chartId: '',
};

const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
Expand Down Expand Up @@ -266,6 +283,9 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
annotationSpecs: getAnnotationSpecsSelector(state),
panelGeoms: computePanelsSelectors(state),
seriesTypes: getSeriesTypes(state),
description: getSettingsSpecSelector(state).description,
useDefaultSummary: getSettingsSpecSelector(state).useDefaultSummary,
markov00 marked this conversation as resolved.
Show resolved Hide resolved
chartId: getChartIdSelector(state),
};
};

Expand Down
77 changes: 77 additions & 0 deletions src/chart_types/xy_chart/state/chart_state.a11y.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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 { getSettingsSpecSelector } from '../../../state/selectors/get_settings_specs';

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 },
],
}),
],
store,
);
MockStore.addSpecs([MockGlobalSpec.settings()], store);
markov00 marked this conversation as resolved.
Show resolved Hide resolved
});
it('should test defaults', () => {
const state = store.getState();
const descriptionValue = getSettingsSpecSelector(state).description;
const defaultGeneratedSeriesTypes = getSettingsSpecSelector(state).useDefaultSummary;
expect(descriptionValue).toBeUndefined();
expect(defaultGeneratedSeriesTypes).toBeTrue();
markov00 marked this conversation as resolved.
Show resolved Hide resolved
});
it('should allow user to set a custom description for chart', () => {
MockStore.addSpecs(
[
MockGlobalSpec.settings({
description: 'This is sample Kibana data',
}),
],
store,
);
const state = store.getState();
const descriptionValue = getSettingsSpecSelector(state).description;
expect(descriptionValue).toBe('This is sample Kibana data');
markov00 marked this conversation as resolved.
Show resolved Hide resolved
});
it('should be able to disable generated descriptions', () => {
MockStore.addSpecs(
[
MockGlobalSpec.settings({
useDefaultSummary: false,
}),
],
store,
);
const state = store.getState();
const disableGeneratedSeriesTypes = getSettingsSpecSelector(state).useDefaultSummary;
expect(disableGeneratedSeriesTypes).toBe(false);
markov00 marked this conversation as resolved.
Show resolved Hide resolved
});
});
22 changes: 12 additions & 10 deletions src/components/__snapshots__/chart.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ exports[`Chart should render the legend name test 1`] = `
<Connect(SpecsParserComponent)>
<SpecsParserComponent specParsed={[Function (anonymous)]} specUnmounted={[Function (anonymous)]}>
<Connect(SpecInstance) debug={true} rendering=\\"svg\\" showLegend={true}>
<SpecInstance debug={true} rendering=\\"svg\\" showLegend={true} upsertSpec={[Function (anonymous)]} removeSpec={[Function (anonymous)]} id=\\"__global__settings___\\" chartType=\\"global\\" specType=\\"settings\\" rotation={0} animateData={true} resizeDebounce={10} tooltip={{...}} externalPointerEvents={{...}} hideDuplicateAxes={false} baseTheme={{...}} brushAxis=\\"x\\" minBrushDelta={2} showLegendExtra={false} legendMaxDepth={Infinity} legendPosition=\\"right\\" />
<SpecInstance debug={true} rendering=\\"svg\\" showLegend={true} upsertSpec={[Function (anonymous)]} removeSpec={[Function (anonymous)]} id=\\"__global__settings___\\" chartType=\\"global\\" specType=\\"settings\\" rotation={0} animateData={true} resizeDebounce={10} tooltip={{...}} externalPointerEvents={{...}} hideDuplicateAxes={false} baseTheme={{...}} brushAxis=\\"x\\" minBrushDelta={2} useDefaultSummary={true} showLegendExtra={false} legendMaxDepth={Infinity} legendPosition=\\"right\\" />
</Connect(SpecInstance)>
<Connect(SpecInstance) id=\\"test\\" data={{...}}>
<SpecInstance id=\\"test\\" data={{...}} upsertSpec={[Function (anonymous)]} removeSpec={[Function (anonymous)]} chartType=\\"xy_axis\\" specType=\\"series\\" seriesType=\\"bar\\" groupId=\\"__global__\\" xScaleType=\\"ordinal\\" yScaleType=\\"linear\\" xAccessor=\\"x\\" yAccessors={{...}} yScaleToDataExtent={false} hideInLegend={false} enableHistogramMode={false} />
Expand All @@ -72,17 +72,19 @@ exports[`Chart should render the legend name test 1`] = `
</Crosshair>
</Connect(Crosshair)>
<Connect(XYChart) forwardStageRef={{...}}>
<XYChart forwardStageRef={{...}} initialized={true} isChartEmpty={false} debug={true} geometries={{...}} geometriesIndex={{...}} theme={{...}} chartContainerDimensions={{...}} highlightedLegendItem={[undefined]} rotation={0} renderingArea={{...}} chartTransform={{...}} axesSpecs={{...}} perPanelAxisGeoms={{...}} perPanelGridLines={{...}} axesStyles={{...}} annotationDimensions={{...}} annotationSpecs={{...}} panelGeoms={{...}} seriesTypes={{...}} onChartRendered={[Function (anonymous)]}>
<XYChart forwardStageRef={{...}} initialized={true} isChartEmpty={false} debug={true} geometries={{...}} geometriesIndex={{...}} theme={{...}} chartContainerDimensions={{...}} highlightedLegendItem={[undefined]} rotation={0} renderingArea={{...}} chartTransform={{...}} axesSpecs={{...}} perPanelAxisGeoms={{...}} perPanelGridLines={{...}} axesStyles={{...}} annotationDimensions={{...}} annotationSpecs={{...}} panelGeoms={{...}} seriesTypes={{...}} description={[undefined]} useDefaultSummary={true} chartId=\\"chart1\\" onChartRendered={[Function (anonymous)]}>
<figure>
<canvas className=\\"echCanvasRenderer\\" width={150} height={200} style={{...}} role=\\"presentation\\">
<dl className=\\"echScreen-reader\\">
<dt>
Chart type
</dt>
<dd>
bar chart
</dd>
</dl>
<div className=\\"echScreenReaderOnly\\">
<dl>
<dt>
Chart type
</dt>
<dd>
bar chart
</dd>
</dl>
</div>
</canvas>
</figure>
</XYChart>
Expand Down
1 change: 1 addition & 0 deletions src/specs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export const DEFAULT_SETTINGS_SPEC: SettingsSpec = {
baseTheme: LIGHT_THEME,
brushAxis: BrushAxis.X,
minBrushDelta: 2,
useDefaultSummary: true,

...DEFAULT_LEGEND_CONFIG,
};
13 changes: 12 additions & 1 deletion src/specs/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,15 @@ export interface SettingsSpec extends Spec, LegendSpec {
* Render component for no results UI
*/
noResults?: ComponentType | ReactChild;
/**
* User can provide a custom description to be read by a screen reader about their chart
*/
description?: string;
/**
* Disable the automated charts series types from being provided for screen readers
* @defaultValue true
*/
useDefaultSummary: boolean;
}

/**
Expand Down Expand Up @@ -608,7 +617,9 @@ export type DefaultSettingsProps =
| 'showLegend'
| 'showLegendExtra'
| 'legendPosition'
| 'legendMaxDepth';
| 'legendMaxDepth'
| 'description'
| 'useDefaultSummary';

/** @public */
export type SettingsSpecProps = Partial<Omit<SettingsSpec, 'chartType' | 'specType' | 'id'>>;
Expand Down
42 changes: 42 additions & 0 deletions stories/test_cases/6_a11y_custom_description.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { boolean, text } from '@storybook/addon-knobs';
import React from 'react';

import { AreaSeries, Chart, ScaleType, Settings } from '../../src';
import { KIBANA_METRICS } from '../../src/utils/data_samples/test_dataset_kibana';

export const Example = () => {
const automatedSeries = boolean('Use the default generated series types of charts for screen readers', true);
const customDescriptionForScreenReaders = text('custom description for screen readers', '');
return (
<Chart className="story-chart">
<Settings description={customDescriptionForScreenReaders} useDefaultSummary={automatedSeries} />
<AreaSeries
id="area"
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={0}
yAccessors={[1]}
data={KIBANA_METRICS.metrics.kibana_os_load[0].data}
/>
</Chart>
);
};
1 change: 1 addition & 0 deletions stories/test_cases/test_cases.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export { Example as chromePathBugFix } from './2_chrome_path_bug_fix';
export { Example as noAxesAnnotationBugFix } from './3_no_axes_annotation';
export { Example as filterZerosInLogFitDomain } from './4_filter_zero_values_log';
export { Example as legendScrollBarSizing } from './5_legend_scroll_bar_sizing';
export { Example as addCustomDescription } from './6_a11y_custom_description';