From ffa88226c4d0c9107df3f324ff99628322a7d1eb Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Wed, 30 Jun 2021 07:06:19 -0600 Subject: [PATCH] feat(a11y): accessible goal and gauge chart (#1174) Fixes #1160 --- .../state/selectors/get_goal_chart_data.ts | 54 +++++++++++++++++++ .../renderer/canvas/partition.tsx | 3 +- .../__snapshots__/chart.test.tsx.snap | 6 +-- .../accessibility/accessibility.test.tsx | 28 ++++++++++ .../src/components/accessibility/index.ts | 1 + .../src/components/accessibility/label.tsx | 34 ++++++++++-- .../accessibility/screen_reader_summary.tsx | 22 ++++++-- .../src/components/accessibility/types.tsx | 28 ++++++++-- 8 files changed, 161 insertions(+), 15 deletions(-) create mode 100644 packages/charts/src/chart_types/goal_chart/state/selectors/get_goal_chart_data.ts diff --git a/packages/charts/src/chart_types/goal_chart/state/selectors/get_goal_chart_data.ts b/packages/charts/src/chart_types/goal_chart/state/selectors/get_goal_chart_data.ts new file mode 100644 index 0000000000..64593a7212 --- /dev/null +++ b/packages/charts/src/chart_types/goal_chart/state/selectors/get_goal_chart_data.ts @@ -0,0 +1,54 @@ +/* + * 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 { createCustomCachedSelector } from '../../../../state/create_selector'; +import { geometries } from './geometries'; + +/** @internal */ +export type GoalChartData = { + maximum: number; + minimum: number; + target: number; + value: number; +}; + +/** @internal */ +export type GoalChartLabels = { + minorLabel: string; + majorLabel: string; +}; + +/** @internal */ +export const getGoalChartDataSelector = createCustomCachedSelector( + [geometries], + (geoms): GoalChartData => { + const goalChartData: GoalChartData = { + maximum: geoms.bulletViewModel.highestValue, + minimum: geoms.bulletViewModel.lowestValue, + target: geoms.bulletViewModel.target, + value: geoms.bulletViewModel.actual, + }; + return goalChartData; + }, +); + +/** @internal */ +export const getGoalChartLabelsSelector = createCustomCachedSelector([geometries], (geoms) => { + return { majorLabel: geoms.bulletViewModel.labelMajor, minorLabel: geoms.bulletViewModel.labelMinor }; +}); diff --git a/packages/charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx b/packages/charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx index fe0f0e1d4c..c7fcc8b7c7 100644 --- a/packages/charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx +++ b/packages/charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx @@ -21,8 +21,7 @@ import React, { MouseEvent, RefObject } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import { ScreenReaderSummary } from '../../../../components/accessibility'; -import { ScreenReaderPartitionTable } from '../../../../components/accessibility/partitions_data_table'; +import { ScreenReaderSummary, ScreenReaderPartitionTable } from '../../../../components/accessibility'; import { clearCanvas } from '../../../../renderers/canvas'; import { SettingsSpec } from '../../../../specs/settings'; import { onChartRendered } from '../../../../state/actions/chart'; diff --git a/packages/charts/src/components/__snapshots__/chart.test.tsx.snap b/packages/charts/src/components/__snapshots__/chart.test.tsx.snap index 303b64418e..d0f198c860 100644 --- a/packages/charts/src/components/__snapshots__/chart.test.tsx.snap +++ b/packages/charts/src/components/__snapshots__/chart.test.tsx.snap @@ -74,11 +74,11 @@ exports[`Chart should render the legend name test 1`] = ` - +
- + - +
Chart type: diff --git a/packages/charts/src/components/accessibility/accessibility.test.tsx b/packages/charts/src/components/accessibility/accessibility.test.tsx index 668cadbc10..9491f5bce4 100644 --- a/packages/charts/src/components/accessibility/accessibility.test.tsx +++ b/packages/charts/src/components/accessibility/accessibility.test.tsx @@ -20,6 +20,8 @@ import { mount } from 'enzyme'; import React from 'react'; +import { Goal } from '../../chart_types/goal_chart/specs'; +import { GoalSubtype } from '../../chart_types/goal_chart/specs/constants'; import { config } from '../../chart_types/partition_chart/layout/config'; import { PartitionLayout } from '../../chart_types/partition_chart/layout/types/config_types'; import { arrayToLookup } from '../../common/color_calcs'; @@ -132,4 +134,30 @@ describe('Accessibility', () => { expect(sunburstLayerWrapper.find('tr').first().text()).toBe('DepthLabelParentValuePercentage'); }); }); + + describe('Goal chart type accessibility', () => { + const goalChartWrapper = mount( + + + , + ); + it('should test defaults for goal charts', () => { + expect(goalChartWrapper.find('.echScreenReaderOnly').first().text()).toBe( + 'Revenue 2020 YTD (thousand USD) Chart type:goal chartMinimum:0Maximum:300Target:$260Value:170', + ); + }); + }); }); diff --git a/packages/charts/src/components/accessibility/index.ts b/packages/charts/src/components/accessibility/index.ts index c4b3e8087d..d20347fcdb 100644 --- a/packages/charts/src/components/accessibility/index.ts +++ b/packages/charts/src/components/accessibility/index.ts @@ -19,3 +19,4 @@ /* @internal */ export { ScreenReaderSummary } from './screen_reader_summary'; +export { ScreenReaderPartitionTable } from './partitions_data_table'; diff --git a/packages/charts/src/components/accessibility/label.tsx b/packages/charts/src/components/accessibility/label.tsx index 151ace8cba..7ba3d383e2 100644 --- a/packages/charts/src/components/accessibility/label.tsx +++ b/packages/charts/src/components/accessibility/label.tsx @@ -19,11 +19,37 @@ import React from 'react'; +import { GoalChartLabels } from '../../chart_types/goal_chart/state/selectors/get_goal_chart_data'; import { A11ySettings } from '../../state/selectors/get_accessibility_config'; +interface ScreenReaderLabelProps { + goalChartLabels?: GoalChartLabels; +} + /** @internal */ -export function ScreenReaderLabel(props: A11ySettings) { - if (!props.label) return null; - const Heading = props.labelHeadingLevel; - return {props.label}; +export function ScreenReaderLabel({ + label, + labelHeadingLevel, + labelId, + goalChartLabels, +}: A11ySettings & ScreenReaderLabelProps) { + const Heading = labelHeadingLevel; + + if (!label && !goalChartLabels?.majorLabel && !goalChartLabels?.minorLabel) return null; + + let unifiedLabel = ''; + if (!label && goalChartLabels?.majorLabel) { + unifiedLabel = goalChartLabels?.majorLabel; + } else if (label && !goalChartLabels?.majorLabel) { + unifiedLabel = label; + } else if (label && goalChartLabels?.majorLabel && label !== goalChartLabels?.majorLabel) { + unifiedLabel = `${label}; Chart visible label: ${goalChartLabels?.majorLabel}`; + } + + return ( + <> + {unifiedLabel && {unifiedLabel}} + {goalChartLabels?.minorLabel &&

{goalChartLabels?.minorLabel}

} + + ); } diff --git a/packages/charts/src/components/accessibility/screen_reader_summary.tsx b/packages/charts/src/components/accessibility/screen_reader_summary.tsx index a2efd1a55a..81e3ae664c 100644 --- a/packages/charts/src/components/accessibility/screen_reader_summary.tsx +++ b/packages/charts/src/components/accessibility/screen_reader_summary.tsx @@ -20,6 +20,12 @@ import React, { memo } from 'react'; import { connect } from 'react-redux'; +import { + getGoalChartDataSelector, + getGoalChartLabelsSelector, + GoalChartData, + GoalChartLabels, +} from '../../chart_types/goal_chart/state/selectors/get_goal_chart_data'; import { GlobalChartState } from '../../state/chart_state'; import { A11ySettings, @@ -35,14 +41,21 @@ import { ScreenReaderTypes } from './types'; interface ScreenReaderSummaryStateProps { a11ySettings: A11ySettings; chartTypeDescription: string; + goalChartData?: GoalChartData; + goalChartLabels?: GoalChartLabels; } -const ScreenReaderSummaryComponent = ({ a11ySettings, chartTypeDescription }: ScreenReaderSummaryStateProps) => { +const ScreenReaderSummaryComponent = ({ + a11ySettings, + chartTypeDescription, + goalChartData, + goalChartLabels, +}: ScreenReaderSummaryStateProps) => { return (
- + - +
); }; @@ -50,6 +63,7 @@ const ScreenReaderSummaryComponent = ({ a11ySettings, chartTypeDescription }: Sc const DEFAULT_SCREEN_READER_SUMMARY = { a11ySettings: DEFAULT_A11Y_SETTINGS, chartTypeDescription: '', + goalChartData: undefined, }; const mapStateToProps = (state: GlobalChartState): ScreenReaderSummaryStateProps => { @@ -59,6 +73,8 @@ const mapStateToProps = (state: GlobalChartState): ScreenReaderSummaryStateProps return { chartTypeDescription: getChartTypeDescriptionSelector(state), a11ySettings: getA11ySettingsSelector(state), + goalChartData: getGoalChartDataSelector(state), + goalChartLabels: getGoalChartLabelsSelector(state), }; }; diff --git a/packages/charts/src/components/accessibility/types.tsx b/packages/charts/src/components/accessibility/types.tsx index aca5c91d40..d4839730f9 100644 --- a/packages/charts/src/components/accessibility/types.tsx +++ b/packages/charts/src/components/accessibility/types.tsx @@ -19,19 +19,41 @@ import React from 'react'; +import { GoalChartData } from '../../chart_types/goal_chart/state/selectors/get_goal_chart_data'; import { A11ySettings } from '../../state/selectors/get_accessibility_config'; interface ScreenReaderTypesProps { chartTypeDescription: string; + goalChartData?: GoalChartData; } /** @internal */ -export function ScreenReaderTypes(props: A11ySettings & ScreenReaderTypesProps) { - if (!props.defaultSummaryId) return null; +export function ScreenReaderTypes({ + goalChartData, + defaultSummaryId, + chartTypeDescription, +}: A11ySettings & ScreenReaderTypesProps) { + if (!defaultSummaryId && !goalChartData) return null; + const validGoalChart = + chartTypeDescription === 'goal chart' || + chartTypeDescription === 'horizontalBullet chart' || + chartTypeDescription === 'verticalBullet chart'; return (
Chart type:
-
{props.chartTypeDescription}
+
{chartTypeDescription}
+ {validGoalChart && goalChartData ? ( + <> +
Minimum:
+
{goalChartData.minimum}
+
Maximum:
+
{goalChartData.maximum}
+
Target:
+
${goalChartData.target}
+
Value:
+
{goalChartData.value}
+ + ) : null}
); }