Skip to content

Commit

Permalink
feat(a11y): accessible goal and gauge chart (#1174)
Browse files Browse the repository at this point in the history
Fixes #1160
  • Loading branch information
rshen91 authored Jun 30, 2021
1 parent 4fd27d1 commit ffa8822
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -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 };
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ exports[`Chart should render the legend name test 1`] = `
<canvas className=\\"echCanvasRenderer\\" width={150} height={200} style={{...}} role=\\"presentation\\" />
</figure>
<Connect(ScreenReaderSummaryComponent)>
<ScreenReaderSummaryComponent chartTypeDescription=\\"bar chart\\" a11ySettings={{...}} dispatch={[Function: dispatch]}>
<ScreenReaderSummaryComponent chartTypeDescription=\\"bar chart\\" a11ySettings={{...}} goalChartData={{...}} goalChartLabels={{...}} dispatch={[Function: dispatch]}>
<div className=\\"echScreenReaderOnly\\">
<ScreenReaderLabel label={[undefined]} labelId={[undefined]} labelHeadingLevel=\\"p\\" description={[undefined]} descriptionId=\\"chart1--defaultSummary\\" defaultSummaryId=\\"chart1--defaultSummary\\" tableCaption={[undefined]} />
<ScreenReaderLabel label={[undefined]} labelId={[undefined]} labelHeadingLevel=\\"p\\" description={[undefined]} descriptionId=\\"chart1--defaultSummary\\" defaultSummaryId=\\"chart1--defaultSummary\\" tableCaption={[undefined]} goalChartLabels={{...}} />
<ScreenReaderDescription label={[undefined]} labelId={[undefined]} labelHeadingLevel=\\"p\\" description={[undefined]} descriptionId=\\"chart1--defaultSummary\\" defaultSummaryId=\\"chart1--defaultSummary\\" tableCaption={[undefined]} />
<ScreenReaderTypes label={[undefined]} labelId={[undefined]} labelHeadingLevel=\\"p\\" description={[undefined]} descriptionId=\\"chart1--defaultSummary\\" defaultSummaryId=\\"chart1--defaultSummary\\" tableCaption={[undefined]} chartTypeDescription=\\"bar chart\\">
<ScreenReaderTypes label={[undefined]} labelId={[undefined]} labelHeadingLevel=\\"p\\" description={[undefined]} descriptionId=\\"chart1--defaultSummary\\" defaultSummaryId=\\"chart1--defaultSummary\\" tableCaption={[undefined]} chartTypeDescription=\\"bar chart\\" goalChartData={{...}}>
<dl>
<dt>
Chart type:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -132,4 +134,30 @@ describe('Accessibility', () => {
expect(sunburstLayerWrapper.find('tr').first().text()).toBe('DepthLabelParentValuePercentage');
});
});

describe('Goal chart type accessibility', () => {
const goalChartWrapper = mount(
<Chart className="story-chart">
<Goal
id="spec_1"
subtype={GoalSubtype.Goal}
base={0}
target={260}
actual={170}
bands={[200, 250, 300]}
ticks={[0, 50, 100, 150, 200, 250, 300]}
labelMajor="Revenue 2020 YTD "
labelMinor="(thousand USD) "
centralMajor="170"
centralMinor=""
config={{ angleStart: Math.PI, angleEnd: 0 }}
/>
</Chart>,
);
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',
);
});
});
});
1 change: 1 addition & 0 deletions packages/charts/src/components/accessibility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@

/* @internal */
export { ScreenReaderSummary } from './screen_reader_summary';
export { ScreenReaderPartitionTable } from './partitions_data_table';
34 changes: 30 additions & 4 deletions packages/charts/src/components/accessibility/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Heading id={props.labelId}>{props.label}</Heading>;
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 && <Heading id={labelId}>{unifiedLabel}</Heading>}
{goalChartLabels?.minorLabel && <p>{goalChartLabels?.minorLabel}</p>}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,21 +41,29 @@ 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 (
<div className="echScreenReaderOnly">
<ScreenReaderLabel {...a11ySettings} />
<ScreenReaderLabel {...a11ySettings} goalChartLabels={goalChartLabels} />
<ScreenReaderDescription {...a11ySettings} />
<ScreenReaderTypes {...a11ySettings} chartTypeDescription={chartTypeDescription} />
<ScreenReaderTypes {...a11ySettings} chartTypeDescription={chartTypeDescription} goalChartData={goalChartData} />
</div>
);
};

const DEFAULT_SCREEN_READER_SUMMARY = {
a11ySettings: DEFAULT_A11Y_SETTINGS,
chartTypeDescription: '',
goalChartData: undefined,
};

const mapStateToProps = (state: GlobalChartState): ScreenReaderSummaryStateProps => {
Expand All @@ -59,6 +73,8 @@ const mapStateToProps = (state: GlobalChartState): ScreenReaderSummaryStateProps
return {
chartTypeDescription: getChartTypeDescriptionSelector(state),
a11ySettings: getA11ySettingsSelector(state),
goalChartData: getGoalChartDataSelector(state),
goalChartLabels: getGoalChartLabelsSelector(state),
};
};

Expand Down
28 changes: 25 additions & 3 deletions packages/charts/src/components/accessibility/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<dl>
<dt>Chart type:</dt>
<dd id={props.defaultSummaryId}>{props.chartTypeDescription}</dd>
<dd id={defaultSummaryId}>{chartTypeDescription}</dd>
{validGoalChart && goalChartData ? (
<>
<dt>Minimum:</dt>
<dd>{goalChartData.minimum}</dd>
<dt>Maximum:</dt>
<dd>{goalChartData.maximum}</dd>
<dt>Target:</dt>
<dd>${goalChartData.target}</dd>
<dd>Value:</dd>
<dt>{goalChartData.value}</dt>
</>
) : null}
</dl>
);
}

0 comments on commit ffa8822

Please sign in to comment.