Skip to content

Commit cfd2ce0

Browse files
authored
chore(refactor): refactors A11Y screen reader description generation into chart selector factories (#2699)
* feat(a11y): enhance accessibility with consolidated screen reader descriptions and axis information * refactor(a11y): replace div with figcaption for better semantic structure and remove unnecessary wrapper div * refactor(a11y): consolidate screen reader logic into centralized selectors * refactor(a11y): implement chart-specific screen reader selectors following ChartSelectors pattern * refactor(a11y): relocate goal chart summary utils to chart type directory * fix(a11y): improve accessibility summary grammar for singular/plural and single values * refactor(a11y): move chart type descriptions to chart-specific screen reader selectors * refactor(a11y): move chart type descriptions to chart-specific screen reader selectors * refactor(a11y): rename PartitionData.data to sections to avoid nested data properties * refactor(a11y): remove chart-specific properties from screen reader summary connector * refactor: move partition screen reader table to chart-specific location * refactor: remove unused ScreenReaderTypes component * refactor(a11y): make ScreenReaderLabel component chart-agnostic by introducing generic ChartLabelData interface * test(a11y): break out accessibility tests into separate files for each chart type * refactor(a11y): move GoalSemanticDescription component to goal chart specific code area * refactor(a11y): consolidate accessibility descriptions into A11ySettings with clearer naming * refactor(a11y): consolidate description rendering in ScreenReaderDescription component * refactor(a11y): simplify description rendering and combine custom/generated descriptions * fix(a11y): move screen reader elements outside canvas and add specific test assertions * refactor(test): consolidate accessibility tests into single file * revert xy_chart figure attributes * update snapshot, revert description tweaks * update charts.api.md * fix(a11y): add legacy format support for screen reader descriptions * test(a11y): update snapshots for improved chart type description format * refactor(a11y): use internal chart state for chart type descriptions * refactor(a11y): consolidate screen reader data selectors into central chart_selectors * refactor(a11y): rename label component to screen_reader_label for clarity * refactor(a11y): consolidate screen reader summary connector into main component * refactor(a11y): consolidate screen reader descriptions using structured SummaryPart objects * test(a11y): update snapshots and test expectations for structured descriptions * refactor(goal_chart): remove unused summary utils * refactor(partition_chart): rename sections to data in screen reader interface * test(partition): add screen reader data selector tests * refactor: move mapStateToProps after component definition for consistency * chore: remove outdated accessibility coverage scripts README * docs: remove redundant comment about description combination * refactor: remove unused chartType field from ScreenReaderSummaryData * refactor(a11y): remove unused chartTypeDescription from screen reader summary * test(snapshots): update chart component snapshots * fix playwright docker image version 1.47.2 * refactor accessibility components into separate concerns * rename SummaryPart to ScreenReaderType and summaryParts prop * revert label naming to major/minor * rename chartSpecificData to screenReaderData * fix passing on default summary id * add goal chart labels to regular screenReaderTypes * fix e2e:a11y assertions. remove screen reader label component * rename ScreenReaderTypes to ScreenReaderItems * fix types for GlobalChartState * rename ScreenReaderType to ScreenReaderItem
1 parent 9b43118 commit cfd2ce0

File tree

20 files changed

+242
-172
lines changed

20 files changed

+242
-172
lines changed

e2e/tests_a11y/goal_chart_a11y.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ test.describe('Goal Chart Accessibility', () => {
1414
test('should generate correct a11y summary for goal chart', async ({ page }) => {
1515
await common.testA11ySummary(page)(
1616
'http://localhost:9001/?path=/story/goal-alpha--minimal-goal',
17-
'Revenue 2020 YTD (thousand USD) Chart type:goal chartMinimum:0Maximum:300Target:260Value:280',
17+
'Chart type:goal chartMajor label:Revenue 2020 YTD Minor label:(thousand USD) Minimum:0Maximum:300Target:260Value:280',
1818
);
1919
});
2020

2121
test('should generate correct a11y summary for gauge chart', async ({ page }) => {
2222
await common.testA11ySummary(page)(
2323
'http://localhost:9001/?path=/story/goal-alpha--gauge-with-target',
24-
'Revenue 2020 YTD (thousand USD) Chart type:goal chartMinimum:0Maximum:300Target:260Value:170',
24+
'Chart type:goal chartMajor label:Revenue 2020 YTD Minor label:(thousand USD) Minimum:0Maximum:300Target:260Value:170',
2525
);
2626
});
2727

2828
test('should generate correct a11y summary for goal chart without target', async ({ page }) => {
2929
await common.testA11ySummary(page)(
3030
'http://localhost:9001/?path=/story/goal-alpha--gaps',
31-
'Revenue 2020 YTD (thousand USD) Chart type:goal chartMinimum:0Maximum:300Target:260Value:280',
31+
'Chart type:goal chartMajor label:Revenue 2020 YTD Minor label:(thousand USD) Minimum:0Maximum:300Target:260Value:280',
3232
);
3333
});
3434

packages/charts/api/charts.api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ export interface ArrayNode extends NodeDescriptor {
246246
}
247247

248248
// @public
249-
export const Axis: FC<SFProps<AxisSpec, "chartType" | "specType", "position" | "hide" | "groupId" | "showOverlappingTicks" | "showOverlappingLabels" | "timeAxisLayerCount", "style" | "title" | "domain" | "maximumFractionDigits" | "ticks" | "tickFormat" | "gridLine" | "labelFormat" | "integersOnly" | "showDuplicatedTicks", "id">>;
249+
export const Axis: FC<SFProps<AxisSpec, "chartType" | "specType", "position" | "hide" | "groupId" | "showOverlappingTicks" | "showOverlappingLabels" | "timeAxisLayerCount", "style" | "title" | "domain" | "maximumFractionDigits" | "tickFormat" | "ticks" | "gridLine" | "labelFormat" | "integersOnly" | "showDuplicatedTicks", "id">>;
250250

251251
// @public (undocumented)
252252
export type AxisId = string;
@@ -3201,7 +3201,7 @@ export type TooltipAction<D extends BaseDatum = Datum, SI extends SeriesIdentifi
32013201
};
32023202

32033203
// @public
3204-
export const tooltipBuildProps: BuildProps<TooltipSpec<any, SeriesIdentifier>, "id" | "chartType" | "specType", "type" | "body" | "footer" | "header" | "actions" | "selectionPrompt" | "actionsLoading" | "noActionsLoaded" | "snap" | "showNullValues" | "actionPrompt" | "pinningPrompt" | "maxTooltipItems" | "maxVisibleTooltipItems", "sort" | "offset" | "unit" | "headerFormatter" | "customTooltip" | "stickTo" | "placement" | "fallbackPlacements" | "boundary" | "boundaryPadding", never>;
3204+
export const tooltipBuildProps: BuildProps<TooltipSpec<any, SeriesIdentifier>, "id" | "chartType" | "specType", "type" | "body" | "footer" | "header" | "actions" | "selectionPrompt" | "actionsLoading" | "noActionsLoaded" | "snap" | "showNullValues" | "actionPrompt" | "pinningPrompt" | "maxTooltipItems" | "maxVisibleTooltipItems", "sort" | "offset" | "headerFormatter" | "unit" | "customTooltip" | "stickTo" | "placement" | "fallbackPlacements" | "boundary" | "boundaryPadding", never>;
32053205

32063206
// @public
32073207
export type TooltipCellStyle = Pick<CSSProperties, 'maxHeight' | 'textAlign' | 'padding' | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft'>;

packages/charts/src/components/accessibility/goal_semantic_description.tsx renamed to packages/charts/src/chart_types/goal_chart/components/goal_semantic_description.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import React, { Fragment } from 'react';
1010

11-
import type { BandViewModel } from '../../chart_types/goal_chart/layout/types/viewmodel_types';
12-
import type { A11ySettings } from '../../state/selectors/get_accessibility_config';
11+
import type { A11ySettings } from '../../../state/selectors/get_accessibility_config';
12+
import type { BandViewModel } from '../layout/types/viewmodel_types';
1313

1414
interface GoalSemanticDescriptionProps {
1515
bandLabels: BandViewModel[];

packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ import { renderCanvas2d } from './canvas_renderers';
1616
import type { Color } from '../../../../common/colors';
1717
import { Colors } from '../../../../common/colors';
1818
import type { Rectangle } from '../../../../common/geometry';
19-
import { GoalSemanticDescription, ScreenReaderSummary } from '../../../../components/accessibility';
19+
import { ScreenReaderSummary } from '../../../../components/accessibility';
2020
import { onChartRendered } from '../../../../state/actions/chart';
2121
import type { GlobalChartState } from '../../../../state/chart_state';
2222
import type { A11ySettings } from '../../../../state/selectors/get_accessibility_config';
2323
import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from '../../../../state/selectors/get_accessibility_config';
2424
import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme';
2525
import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized';
2626
import type { Dimensions } from '../../../../utils/dimensions';
27+
import { GoalSemanticDescription } from '../../components/goal_semantic_description';
2728
import type { BandViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
2829
import { nullShapeViewModel } from '../../layout/types/viewmodel_types';
2930
import type { Mark } from '../../layout/viewmodel/geoms';
@@ -138,10 +139,9 @@ class Component extends React.Component<Props> {
138139
}}
139140
// eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role
140141
role="presentation"
141-
>
142-
<ScreenReaderSummary />
143-
<GoalSemanticDescription bandLabels={bandLabels} firstValue={firstValue} {...a11ySettings} />
144-
</canvas>
142+
/>
143+
<ScreenReaderSummary />
144+
<GoalSemanticDescription bandLabels={bandLabels} firstValue={firstValue} {...a11ySettings} />
145145
</figure>
146146
);
147147
}

packages/charts/src/chart_types/goal_chart/state/chart_selectors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { getChartTypeDescriptionSelector } from './selectors/get_chart_type_description';
1010
import { getGoalSpecSelector } from './selectors/get_goal_spec';
11+
import { getScreenReaderDataSelector } from './selectors/get_screen_reader_data';
1112
import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible';
1213
import { createOnElementClickCaller } from './selectors/on_element_click_caller';
1314
import { createOnElementOutCaller } from './selectors/on_element_out_caller';
@@ -46,6 +47,7 @@ export const chartSelectorsFactory = createChartSelectorsFactory(
4647
},
4748

4849
getChartTypeDescription: getChartTypeDescriptionSelector,
50+
getScreenReaderData: getScreenReaderDataSelector,
4951

5052
// TODO enable for small multiples
5153
canDisplayChartTitles: () => false,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { getGoalChartDataSelector, getGoalChartLabelsSelector } from './get_goal_chart_data';
10+
import type { ChartSpecificScreenReaderData, ScreenReaderItem } from '../../../../state/chart_selectors';
11+
import type { GlobalChartState } from '../../../../state/chart_state';
12+
import { createCustomCachedSelector } from '../../../../state/create_selector';
13+
import { getA11ySettingsSelector } from '../../../../state/selectors/get_accessibility_config';
14+
import { getInternalChartStateSelector } from '../../../../state/selectors/get_internal_chart_state';
15+
16+
/** @internal */
17+
export const getScreenReaderDataSelector = createCustomCachedSelector(
18+
[
19+
getGoalChartDataSelector,
20+
getGoalChartLabelsSelector,
21+
getInternalChartStateSelector,
22+
getA11ySettingsSelector,
23+
(state: GlobalChartState) => state,
24+
],
25+
(goalChartData, goalChartLabels, internalChartState, a11ySettings, state): ChartSpecificScreenReaderData => {
26+
const screenReaderItems: ScreenReaderItem[] = [];
27+
28+
// Add chart type description first
29+
const chartTypeDescription = internalChartState?.getChartTypeDescription(state);
30+
if (chartTypeDescription) {
31+
screenReaderItems.push({
32+
label: 'Chart type',
33+
id: a11ySettings.defaultSummaryId,
34+
value: chartTypeDescription,
35+
});
36+
}
37+
38+
// Add goal chart specific parts
39+
if (goalChartData && !isNaN(goalChartData.maximum)) {
40+
if (goalChartLabels.majorLabel) {
41+
screenReaderItems.push({
42+
label: 'Major label',
43+
value: goalChartLabels.majorLabel,
44+
});
45+
}
46+
if (goalChartLabels.minorLabel) {
47+
screenReaderItems.push({
48+
label: 'Minor label',
49+
value: goalChartLabels.minorLabel,
50+
});
51+
}
52+
screenReaderItems.push(
53+
{
54+
label: 'Minimum',
55+
value: goalChartData.minimum.toString(),
56+
},
57+
{
58+
label: 'Maximum',
59+
value: goalChartData.maximum.toString(),
60+
},
61+
{
62+
label: 'Target',
63+
value: goalChartData.target?.toString() ?? 'N/A',
64+
},
65+
{
66+
label: 'Value',
67+
value: goalChartData.value.toString(),
68+
},
69+
);
70+
}
71+
72+
return { screenReaderItems };
73+
},
74+
);

packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,8 @@ class Component extends React.Component<Props> {
136136
}}
137137
// eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role
138138
role="presentation"
139-
>
140-
<ScreenReaderSummary />
141-
</canvas>
139+
/>
140+
<ScreenReaderSummary />
142141
</figure>
143142
);
144143
}

packages/charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { renderPartitionCanvas2d } from './canvas_renderers';
1717
import { renderWrappedPartitionCanvas2d } from './canvas_wrapped_renderers';
1818
import type { Color } from '../../../../common/colors';
1919
import { Colors } from '../../../../common/colors';
20-
import { ScreenReaderSummary, ScreenReaderPartitionTable } from '../../../../components/accessibility';
20+
import { ScreenReaderSummary } from '../../../../components/accessibility';
2121
import { clearCanvas } from '../../../../renderers/canvas';
2222
import type { SettingsSpec } from '../../../../specs/settings';
2323
import { onChartRendered } from '../../../../state/actions/chart';
@@ -35,6 +35,7 @@ import { hasMostlyRTLLabels, nullShapeViewModel } from '../../layout/types/viewm
3535
import { INPUT_KEY } from '../../layout/utils/group_by_rollup';
3636
import { isSimpleLinear, isWaffle } from '../../layout/viewmodel/viewmodel';
3737
import { partitionDrilldownFocus, partitionMultiGeometries } from '../../state/selectors/geometries';
38+
import { ScreenReaderPartitionTable } from '../dom/screen_reader_partition_table';
3839

3940
/** @internal */
4041
export interface ContinuousDomainFocus {

packages/charts/src/components/accessibility/partitions_data_table.tsx renamed to packages/charts/src/chart_types/partition_chart/renderer/dom/screen_reader_partition_table.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99
import React, { useRef, memo, useState } from 'react';
1010
import { connect } from 'react-redux';
1111

12-
import type { PartitionData } from '../../chart_types/partition_chart/state/selectors/get_screen_reader_data';
13-
import { getScreenReaderDataSelector } from '../../chart_types/partition_chart/state/selectors/get_screen_reader_data';
14-
import type { SettingsSpec } from '../../specs/settings';
15-
import type { GlobalChartState } from '../../state/chart_state';
16-
import type { A11ySettings } from '../../state/selectors/get_accessibility_config';
17-
import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from '../../state/selectors/get_accessibility_config';
18-
import { getInternalIsInitializedSelector, InitStatus } from '../../state/selectors/get_internal_is_intialized';
19-
import { getSettingsSpecSelector } from '../../state/selectors/get_settings_spec';
20-
import { isNil } from '../../utils/common';
12+
import type { SettingsSpec } from '../../../../specs/settings';
13+
import type { GlobalChartState } from '../../../../state/chart_state';
14+
import type { A11ySettings } from '../../../../state/selectors/get_accessibility_config';
15+
import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from '../../../../state/selectors/get_accessibility_config';
16+
import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized';
17+
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
18+
import { isNil } from '../../../../utils/common';
19+
import { getPartitionScreenReaderDataSelector } from '../../state/selectors/get_screen_reader_data';
20+
import type { PartitionData } from '../../state/selectors/get_screen_reader_data';
2121

2222
interface ScreenReaderPartitionTableProps {
2323
a11ySettings: A11ySettings;
@@ -131,7 +131,7 @@ const mapStateToProps = (state: GlobalChartState): ScreenReaderPartitionTablePro
131131
}
132132
return {
133133
a11ySettings: getA11ySettingsSelector(state),
134-
partitionData: getScreenReaderDataSelector(state),
134+
partitionData: getPartitionScreenReaderDataSelector(state),
135135
debug: getSettingsSpecSelector(state).debug,
136136
};
137137
};

packages/charts/src/chart_types/partition_chart/state/selectors/get_screen_reader_data.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import type { Store } from 'redux';
1010

11-
import { getScreenReaderDataSelector } from './get_screen_reader_data';
11+
import { getPartitionScreenReaderDataSelector } from './get_screen_reader_data';
1212
import { MockSeriesSpec } from '../../../../mocks/specs/specs';
1313
import { MockStore } from '../../../../mocks/store';
1414
import type { GlobalChartState } from '../../../../state/chart_state';
@@ -65,7 +65,7 @@ describe('Get screen reader data', () => {
6565

6666
it('should test defaults', () => {
6767
MockStore.addSpecs([spec1], store);
68-
const expected = getScreenReaderDataSelector(store.getState());
68+
const expected = getPartitionScreenReaderDataSelector(store.getState());
6969
expect(expected).toEqual({
7070
data: [
7171
{ depth: 1, label: 'aaa', panelTitle: '', parentName: 'none', percentage: '100%', value: 3, valueText: '3' },
@@ -81,7 +81,7 @@ describe('Get screen reader data', () => {
8181
});
8282
it('should compute screen reader data for no slices in pie', () => {
8383
MockStore.addSpecs([specNoSlice], store);
84-
const expected = getScreenReaderDataSelector(store.getState());
84+
const expected = getPartitionScreenReaderDataSelector(store.getState());
8585
expect(expected).toEqual({
8686
data: [],
8787
hasMultipleLayers: true,

0 commit comments

Comments
 (0)