Skip to content

Commit

Permalink
feat(a11y): allow user to pass custom description for screen readers (#…
Browse files Browse the repository at this point in the history
…1111)

Fixes #1097
  • Loading branch information
rshen91 committed Apr 14, 2021
1 parent 3b712ab commit 2ee1b91
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 18 deletions.
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.
34 changes: 28 additions & 6 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,7 @@ 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>
<canvas
Expand All @@ -178,11 +185,19 @@ class XYChartComponent extends React.Component<XYChartProps> {
}}
// eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role
role="presentation"
{...(description ? { 'aria-describedby': chartIdDescription } : {})}
>
<dl className="echScreen-reader">
<dt>Chart type</dt>
<dd>{chartSeriesTypes}</dd>
</dl>
{(description || useDefaultSummary) && (
<div className="echScreenReaderOnly">
{description && <p id={chartIdDescription}>{description}</p>}
{useDefaultSummary && (
<dl>
<dt>Chart type</dt>
<dd>{chartSeriesTypes}</dd>
</dl>
)}
</div>
)}
</canvas>
</figure>
);
Expand Down Expand Up @@ -237,6 +252,9 @@ const DEFAULT_PROPS: ReactiveChartStateProps = {
annotationSpecs: [],
panelGeoms: [],
seriesTypes: new Set(),
description: undefined,
useDefaultSummary: true,
chartId: '',
};

const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
Expand All @@ -245,11 +263,12 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
}

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

return {
initialized: true,
isChartEmpty: isChartEmptySelector(state),
debug: getSettingsSpecSelector(state).debug,
debug,
geometries,
geometriesIndex,
theme: getChartThemeSelector(state),
Expand All @@ -266,6 +285,9 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
annotationSpecs: getAnnotationSpecsSelector(state),
panelGeoms: computePanelsSelectors(state),
seriesTypes: getSeriesTypes(state),
description,
useDefaultSummary,
chartId: getChartIdSelector(state),
};
};

Expand Down
76 changes: 76 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,76 @@
/*
* 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 },
],
}),
MockGlobalSpec.settings(),
],
store,
);
});
it('should test defaults', () => {
const state = store.getState();
const { description, useDefaultSummary } = getSettingsSpecSelector(state);
expect(description).toBeUndefined();
expect(useDefaultSummary).toBeTrue();
});
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 { description } = getSettingsSpecSelector(state);
expect(description).toBe('This is sample Kibana data');
});
it('should be able to disable generated descriptions', () => {
MockStore.addSpecs(
[
MockGlobalSpec.settings({
useDefaultSummary: false,
}),
],
store,
);
const state = store.getState();
const { useDefaultSummary } = getSettingsSpecSelector(state);
expect(useDefaultSummary).toBe(false);
});
});
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';

0 comments on commit 2ee1b91

Please sign in to comment.