From 815cf39873e3e1f0a526dd88bb06c2b87f22f9e8 Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Mon, 12 Apr 2021 09:48:21 -0600 Subject: [PATCH] feat(a11y): improve chart figure (#1104) --- .playground/playground.tsx | 35 ++++++++------- integration/tests/accessibility.test.ts | 45 +++++++++++++++++++ .../xy_chart/renderer/canvas/xy_chart.tsx | 42 +++++++++++------ .../xy_chart/renderer/dom/_index.scss | 1 + .../xy_chart/renderer/dom/_screen_reader.scss | 8 ++++ .../state/selectors/get_series_types.ts | 34 ++++++++++++++ .../__snapshots__/chart.test.tsx.snap | 15 ++++++- 7 files changed, 148 insertions(+), 32 deletions(-) create mode 100644 integration/tests/accessibility.test.ts create mode 100644 src/chart_types/xy_chart/renderer/dom/_screen_reader.scss create mode 100644 src/chart_types/xy_chart/state/selectors/get_series_types.ts diff --git a/.playground/playground.tsx b/.playground/playground.tsx index 5332b367ea..c163367dd2 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -19,29 +19,32 @@ import React from 'react'; -import { Chart, BarSeries, ScaleType, LineAnnotation, AnnotationDomainTypes, LineAnnotationDatum } from '../src'; +import { Chart, AreaSeries, LineSeries, BarSeries, ScaleType } from '../src'; -function generateAnnotationData(values: any[]): LineAnnotationDatum[] { - return values.map((value, index) => ({ dataValue: value, details: `detail-${index}` })); -} export class Playground extends React.Component { render() { return (
- hello
} - // markerPosition="top" + - Horizontal} - // markerPosition="right" + { + it('should include the series types if one type of series', async () => { + const tree = await common.testAccessibilityTree( + 'http://localhost:9001/iframe.html?id=annotations-lines--x-continuous-domain', + '.echCanvasRenderer', + ); + // the legend has bars and lines as value.descriptions not value.name + const hasTextOfChartTypes = tree.children.filter((value) => { + return value.name === 'bar chart'; + }); + expect(hasTextOfChartTypes[0].name).toBe('bar chart'); + }); + it('should include the series types if multiple types of series', async () => { + const tree = await common.testAccessibilityTree( + 'http://localhost:9001/iframe.html?id=mixed-charts--bars-and-lines', + '.echCanvasRenderer', + ); + // the legend has bars and lines as value.descriptions not value.name + const hasTextOfChartTypes = tree.children.filter((value) => { + return value.name === 'Mixed chart: bar and line chart'; + }); + expect(hasTextOfChartTypes[0].name).toBe('Mixed chart: bar and line chart'); + }); +}); diff --git a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index 94be7d259e..40a174a841 100644 --- a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -48,12 +48,13 @@ import { import { computeSeriesGeometriesSelector } from '../../state/selectors/compute_series_geometries'; import { getAxesStylesSelector } from '../../state/selectors/get_axis_styles'; import { getHighlightedSeriesSelector } from '../../state/selectors/get_highlighted_series'; +import { getSeriesTypes } from '../../state/selectors/get_series_types'; import { getAnnotationSpecsSelector, getAxisSpecsSelector } from '../../state/selectors/get_specs'; import { isChartEmptySelector } from '../../state/selectors/is_chart_empty'; import { Geometries, Transform } from '../../state/utils/types'; import { LinesGrid } from '../../utils/grid_lines'; import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; -import { AxisSpec, AnnotationSpec } from '../../utils/specs'; +import { AxisSpec, AnnotationSpec, SeriesType } from '../../utils/specs'; import { renderXYChartCanvas2d } from './renderers'; /** @internal */ @@ -76,6 +77,7 @@ export interface ReactiveChartStateProps { annotationDimensions: Map; annotationSpecs: AnnotationSpec[]; panelGeoms: PanelGeoms; + seriesTypes: Set; } interface ReactiveChartDispatchProps { @@ -152,6 +154,7 @@ class XYChartComponent extends React.Component { initialized, isChartEmpty, chartContainerDimensions: { width, height }, + seriesTypes, } = this.props; if (!initialized || isChartEmpty) { @@ -159,20 +162,29 @@ class XYChartComponent extends React.Component { return null; } + const chartSeriesTypes = + seriesTypes.size > 1 ? `Mixed chart: ${[...seriesTypes].join(' and ')} chart` : `${[...seriesTypes]} chart`; + return ( - +
+ +
+
Chart type
+
{chartSeriesTypes}
+
+
+
); } } @@ -224,6 +236,7 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { annotationDimensions: new Map(), annotationSpecs: [], panelGeoms: [], + seriesTypes: new Set(), }; const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { @@ -252,6 +265,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { annotationDimensions: computeAnnotationDimensionsSelector(state), annotationSpecs: getAnnotationSpecsSelector(state), panelGeoms: computePanelsSelectors(state), + seriesTypes: getSeriesTypes(state), }; }; diff --git a/src/chart_types/xy_chart/renderer/dom/_index.scss b/src/chart_types/xy_chart/renderer/dom/_index.scss index 548fb3f7de..df6c1c3101 100644 --- a/src/chart_types/xy_chart/renderer/dom/_index.scss +++ b/src/chart_types/xy_chart/renderer/dom/_index.scss @@ -1,3 +1,4 @@ @import 'highlighter'; @import 'crosshair'; +@import 'screen_reader'; @import 'annotations/index'; diff --git a/src/chart_types/xy_chart/renderer/dom/_screen_reader.scss b/src/chart_types/xy_chart/renderer/dom/_screen_reader.scss new file mode 100644 index 0000000000..0bd8bafaf9 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/dom/_screen_reader.scss @@ -0,0 +1,8 @@ +.echScreenReaderOnly { + position: absolute; + left: -10000px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; +} diff --git a/src/chart_types/xy_chart/state/selectors/get_series_types.ts b/src/chart_types/xy_chart/state/selectors/get_series_types.ts new file mode 100644 index 0000000000..e6ce67d61c --- /dev/null +++ b/src/chart_types/xy_chart/state/selectors/get_series_types.ts @@ -0,0 +1,34 @@ +/* + * 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 createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { SeriesType } from '../../utils/specs'; +import { getSeriesSpecsSelector } from './get_specs'; + +/** @internal */ +export const getSeriesTypes = createCachedSelector( + [getSeriesSpecsSelector], + (specs): Set => { + const seriesTypes = new Set(); + specs.forEach((value) => seriesTypes.add(value.seriesType)); + return seriesTypes; + }, +)(getChartIdSelector); diff --git a/src/components/__snapshots__/chart.test.tsx.snap b/src/components/__snapshots__/chart.test.tsx.snap index 496feecdee..6c1e2f571a 100644 --- a/src/components/__snapshots__/chart.test.tsx.snap +++ b/src/components/__snapshots__/chart.test.tsx.snap @@ -72,8 +72,19 @@ exports[`Chart should render the legend name test 1`] = ` - - + +
+ +
+
+ Chart type +
+
+ bar chart +
+
+
+