Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
cbe864d
feat(a11y): enhance accessibility with consolidated screen reader des…
walterra Jun 27, 2025
a298272
refactor(a11y): replace div with figcaption for better semantic struc…
walterra Jun 30, 2025
a1d4b34
refactor(a11y): consolidate screen reader logic into centralized sele…
walterra Jun 30, 2025
6b1d44f
refactor(a11y): implement chart-specific screen reader selectors foll…
walterra Jul 2, 2025
b242a5d
refactor(a11y): relocate goal chart summary utils to chart type direc…
walterra Jul 3, 2025
0593dc5
fix(a11y): improve accessibility summary grammar for singular/plural …
walterra Jul 3, 2025
bc90145
refactor(a11y): move chart type descriptions to chart-specific screen…
walterra Jul 3, 2025
a3b34d3
refactor(a11y): move chart type descriptions to chart-specific screen…
walterra Jul 3, 2025
ec2f64e
refactor(a11y): rename PartitionData.data to sections to avoid nested…
walterra Jul 3, 2025
45cc73e
refactor(a11y): remove chart-specific properties from screen reader s…
walterra Jul 3, 2025
fd3178f
refactor: move partition screen reader table to chart-specific location
walterra Jul 3, 2025
fa6d612
refactor: remove unused ScreenReaderTypes component
walterra Jul 3, 2025
5590b67
refactor(a11y): make ScreenReaderLabel component chart-agnostic by in…
walterra Jul 3, 2025
355c19f
test(a11y): break out accessibility tests into separate files for eac…
walterra Jul 3, 2025
1e64d08
refactor(a11y): move GoalSemanticDescription component to goal chart …
walterra Jul 3, 2025
01c1873
refactor(a11y): consolidate accessibility descriptions into A11ySetti…
walterra Jul 7, 2025
17f5eff
refactor(a11y): consolidate description rendering in ScreenReaderDesc…
walterra Jul 7, 2025
e9ae646
refactor(a11y): simplify description rendering and combine custom/gen…
walterra Jul 7, 2025
e0e6e09
fix(a11y): move screen reader elements outside canvas and add specifi…
walterra Jul 8, 2025
7de6486
refactor(test): consolidate accessibility tests into single file
walterra Aug 11, 2025
45c9b2f
revert xy_chart figure attributes
walterra Aug 11, 2025
a71cbe3
update snapshot, revert description tweaks
walterra Aug 11, 2025
69b7954
update charts.api.md
walterra Aug 11, 2025
e679ab4
fix(a11y): add legacy format support for screen reader descriptions
walterra Aug 12, 2025
e2a6919
test(a11y): update snapshots for improved chart type description format
walterra Aug 12, 2025
6ae8ca7
refactor(a11y): use internal chart state for chart type descriptions
walterra Aug 12, 2025
3a713e7
refactor(a11y): consolidate screen reader data selectors into central…
walterra Aug 12, 2025
d38a384
refactor(a11y): rename label component to screen_reader_label for cla…
walterra Aug 12, 2025
8bc501a
refactor(a11y): consolidate screen reader summary connector into main…
walterra Aug 12, 2025
8ed5216
refactor(a11y): consolidate screen reader descriptions using structur…
walterra Aug 12, 2025
500997b
test(a11y): update snapshots and test expectations for structured des…
walterra Aug 12, 2025
dd9348d
refactor(goal_chart): remove unused summary utils
walterra Aug 12, 2025
277a271
refactor(partition_chart): rename sections to data in screen reader i…
walterra Aug 12, 2025
e71fd03
test(partition): add screen reader data selector tests
walterra Aug 12, 2025
dc0602f
refactor: move mapStateToProps after component definition for consist…
walterra Aug 12, 2025
dbc334a
chore: remove outdated accessibility coverage scripts README
walterra Aug 12, 2025
c206d15
docs: remove redundant comment about description combination
walterra Aug 12, 2025
dd83ae7
refactor: remove unused chartType field from ScreenReaderSummaryData
walterra Aug 12, 2025
f1c1c4a
refactor(a11y): remove unused chartTypeDescription from screen reader…
walterra Aug 12, 2025
6a29ac7
test(snapshots): update chart component snapshots
walterra Aug 12, 2025
da52b18
fix playwright docker image version 1.47.2
walterra Aug 13, 2025
e53be47
refactor accessibility components into separate concerns
walterra Aug 14, 2025
e42bdfd
rename SummaryPart to ScreenReaderType and summaryParts prop
walterra Aug 14, 2025
b093edf
revert label naming to major/minor
walterra Aug 14, 2025
b6fd07b
rename chartSpecificData to screenReaderData
walterra Aug 14, 2025
fd7df6d
fix passing on default summary id
walterra Aug 14, 2025
8638987
Merge branch 'main' into a11y-refactor-screen-reader-descriptions
walterra Aug 14, 2025
204e199
Merge branch 'main' into a11y-refactor-screen-reader-descriptions
walterra Aug 15, 2025
862b82d
Merge branch 'main' into a11y-refactor-screen-reader-descriptions
walterra Aug 27, 2025
a0342f3
Merge branch 'main' into a11y-refactor-screen-reader-descriptions
walterra Sep 9, 2025
8fb663c
add goal chart labels to regular screenReaderTypes
walterra Sep 9, 2025
633a922
fix e2e:a11y assertions. remove screen reader label component
walterra Sep 9, 2025
eaa52c9
rename ScreenReaderTypes to ScreenReaderItems
walterra Sep 9, 2025
c21d298
fix types for GlobalChartState
walterra Sep 9, 2025
8fc16d7
rename ScreenReaderType to ScreenReaderItem
walterra Sep 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions e2e/tests_a11y/goal_chart_a11y.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ test.describe('Goal Chart Accessibility', () => {
test('should generate correct a11y summary for goal chart', async ({ page }) => {
await common.testA11ySummary(page)(
'http://localhost:9001/?path=/story/goal-alpha--minimal-goal',
'Revenue 2020 YTD (thousand USD) Chart type:goal chartMinimum:0Maximum:300Target:260Value:280',
'Chart type:goal chartMajor label:Revenue 2020 YTD Minor label:(thousand USD) Minimum:0Maximum:300Target:260Value:280',
);
});

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

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

Expand Down
4 changes: 2 additions & 2 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export interface ArrayNode extends NodeDescriptor {
}

// @public
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">>;
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">>;

// @public (undocumented)
export type AxisId = string;
Expand Down Expand Up @@ -3201,7 +3201,7 @@ export type TooltipAction<D extends BaseDatum = Datum, SI extends SeriesIdentifi
};

// @public
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>;
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>;

// @public
export type TooltipCellStyle = Pick<CSSProperties, 'maxHeight' | 'textAlign' | 'padding' | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft'>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

import React, { Fragment } from 'react';

import type { BandViewModel } from '../../chart_types/goal_chart/layout/types/viewmodel_types';
import type { A11ySettings } from '../../state/selectors/get_accessibility_config';
import type { A11ySettings } from '../../../state/selectors/get_accessibility_config';
import type { BandViewModel } from '../layout/types/viewmodel_types';

interface GoalSemanticDescriptionProps {
bandLabels: BandViewModel[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ import { renderCanvas2d } from './canvas_renderers';
import type { Color } from '../../../../common/colors';
import { Colors } from '../../../../common/colors';
import type { Rectangle } from '../../../../common/geometry';
import { GoalSemanticDescription, ScreenReaderSummary } from '../../../../components/accessibility';
import { ScreenReaderSummary } from '../../../../components/accessibility';
import { onChartRendered } from '../../../../state/actions/chart';
import type { GlobalChartState } from '../../../../state/chart_state';
import type { A11ySettings } from '../../../../state/selectors/get_accessibility_config';
import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from '../../../../state/selectors/get_accessibility_config';
import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme';
import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized';
import type { Dimensions } from '../../../../utils/dimensions';
import { GoalSemanticDescription } from '../../components/goal_semantic_description';
import type { BandViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types';
import { nullShapeViewModel } from '../../layout/types/viewmodel_types';
import type { Mark } from '../../layout/viewmodel/geoms';
Expand Down Expand Up @@ -138,10 +139,9 @@ class Component extends React.Component<Props> {
}}
// eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role
role="presentation"
>
<ScreenReaderSummary />
<GoalSemanticDescription bandLabels={bandLabels} firstValue={firstValue} {...a11ySettings} />
</canvas>
/>
<ScreenReaderSummary />
<GoalSemanticDescription bandLabels={bandLabels} firstValue={firstValue} {...a11ySettings} />
</figure>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { getChartTypeDescriptionSelector } from './selectors/get_chart_type_description';
import { getGoalSpecSelector } from './selectors/get_goal_spec';
import { getScreenReaderDataSelector } from './selectors/get_screen_reader_data';
import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible';
import { createOnElementClickCaller } from './selectors/on_element_click_caller';
import { createOnElementOutCaller } from './selectors/on_element_out_caller';
Expand Down Expand Up @@ -46,6 +47,7 @@ export const chartSelectorsFactory = createChartSelectorsFactory(
},

getChartTypeDescription: getChartTypeDescriptionSelector,
getScreenReaderData: getScreenReaderDataSelector,

// TODO enable for small multiples
canDisplayChartTitles: () => false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { getGoalChartDataSelector, getGoalChartLabelsSelector } from './get_goal_chart_data';
import type { ChartSpecificScreenReaderData, ScreenReaderItem } from '../../../../state/chart_selectors';
import type { GlobalChartState } from '../../../../state/chart_state';
import { createCustomCachedSelector } from '../../../../state/create_selector';
import { getA11ySettingsSelector } from '../../../../state/selectors/get_accessibility_config';
import { getInternalChartStateSelector } from '../../../../state/selectors/get_internal_chart_state';

/** @internal */
export const getScreenReaderDataSelector = createCustomCachedSelector(
[
getGoalChartDataSelector,
getGoalChartLabelsSelector,
getInternalChartStateSelector,
getA11ySettingsSelector,
(state: GlobalChartState) => state,
],
(goalChartData, goalChartLabels, internalChartState, a11ySettings, state): ChartSpecificScreenReaderData => {
const screenReaderItems: ScreenReaderItem[] = [];

// Add chart type description first
const chartTypeDescription = internalChartState?.getChartTypeDescription(state);
if (chartTypeDescription) {
screenReaderItems.push({
label: 'Chart type',
id: a11ySettings.defaultSummaryId,
value: chartTypeDescription,
});
}

// Add goal chart specific parts
if (goalChartData && !isNaN(goalChartData.maximum)) {
if (goalChartLabels.majorLabel) {
screenReaderItems.push({
label: 'Major label',
value: goalChartLabels.majorLabel,
});
}
if (goalChartLabels.minorLabel) {
screenReaderItems.push({
label: 'Minor label',
value: goalChartLabels.minorLabel,
});
}
screenReaderItems.push(
{
label: 'Minimum',
value: goalChartData.minimum.toString(),
},
{
label: 'Maximum',
value: goalChartData.maximum.toString(),
},
{
label: 'Target',
value: goalChartData.target?.toString() ?? 'N/A',
},
{
label: 'Value',
value: goalChartData.value.toString(),
},
);
}

return { screenReaderItems };
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,8 @@ class Component extends React.Component<Props> {
}}
// eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role
role="presentation"
>
<ScreenReaderSummary />
</canvas>
/>
<ScreenReaderSummary />
</figure>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { renderPartitionCanvas2d } from './canvas_renderers';
import { renderWrappedPartitionCanvas2d } from './canvas_wrapped_renderers';
import type { Color } from '../../../../common/colors';
import { Colors } from '../../../../common/colors';
import { ScreenReaderSummary, ScreenReaderPartitionTable } from '../../../../components/accessibility';
import { ScreenReaderSummary } from '../../../../components/accessibility';
import { clearCanvas } from '../../../../renderers/canvas';
import type { SettingsSpec } from '../../../../specs/settings';
import { onChartRendered } from '../../../../state/actions/chart';
Expand All @@ -35,6 +35,7 @@ import { hasMostlyRTLLabels, nullShapeViewModel } from '../../layout/types/viewm
import { INPUT_KEY } from '../../layout/utils/group_by_rollup';
import { isSimpleLinear, isWaffle } from '../../layout/viewmodel/viewmodel';
import { partitionDrilldownFocus, partitionMultiGeometries } from '../../state/selectors/geometries';
import { ScreenReaderPartitionTable } from '../dom/screen_reader_partition_table';

/** @internal */
export interface ContinuousDomainFocus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
import React, { useRef, memo, useState } from 'react';
import { connect } from 'react-redux';

import type { PartitionData } from '../../chart_types/partition_chart/state/selectors/get_screen_reader_data';
import { getScreenReaderDataSelector } from '../../chart_types/partition_chart/state/selectors/get_screen_reader_data';
import type { SettingsSpec } from '../../specs/settings';
import type { GlobalChartState } from '../../state/chart_state';
import type { A11ySettings } from '../../state/selectors/get_accessibility_config';
import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from '../../state/selectors/get_accessibility_config';
import { getInternalIsInitializedSelector, InitStatus } from '../../state/selectors/get_internal_is_intialized';
import { getSettingsSpecSelector } from '../../state/selectors/get_settings_spec';
import { isNil } from '../../utils/common';
import type { SettingsSpec } from '../../../../specs/settings';
import type { GlobalChartState } from '../../../../state/chart_state';
import type { A11ySettings } from '../../../../state/selectors/get_accessibility_config';
import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from '../../../../state/selectors/get_accessibility_config';
import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
import { isNil } from '../../../../utils/common';
import { getPartitionScreenReaderDataSelector } from '../../state/selectors/get_screen_reader_data';
import type { PartitionData } from '../../state/selectors/get_screen_reader_data';

interface ScreenReaderPartitionTableProps {
a11ySettings: A11ySettings;
Expand Down Expand Up @@ -131,7 +131,7 @@ const mapStateToProps = (state: GlobalChartState): ScreenReaderPartitionTablePro
}
return {
a11ySettings: getA11ySettingsSelector(state),
partitionData: getScreenReaderDataSelector(state),
partitionData: getPartitionScreenReaderDataSelector(state),
debug: getSettingsSpecSelector(state).debug,
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import type { Store } from 'redux';

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

it('should test defaults', () => {
MockStore.addSpecs([spec1], store);
const expected = getScreenReaderDataSelector(store.getState());
const expected = getPartitionScreenReaderDataSelector(store.getState());
expect(expected).toEqual({
data: [
{ depth: 1, label: 'aaa', panelTitle: '', parentName: 'none', percentage: '100%', value: 3, valueText: '3' },
Expand All @@ -81,7 +81,7 @@ describe('Get screen reader data', () => {
});
it('should compute screen reader data for no slices in pie', () => {
MockStore.addSpecs([specNoSlice], store);
const expected = getScreenReaderDataSelector(store.getState());
const expected = getPartitionScreenReaderDataSelector(store.getState());
expect(expected).toEqual({
data: [],
hasMultipleLayers: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const getScreenReaderDataForPartitions = (
};

/** @internal */
export const getScreenReaderDataSelector = createCustomCachedSelector(
export const getPartitionScreenReaderDataSelector = createCustomCachedSelector(
[getPartitionSpecs, partitionMultiGeometries],
(specs, shapeViewModel): PartitionData => {
if (specs.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe('Accessibility', () => {
</Chart>,
);
expect(screen.getByTestId('echScreenReaderSummary').textContent).toBe(
'Revenue 2020 YTD (thousand USD) Chart type:goal chartMinimum:0Maximum:300Target:260Value:170',
'Chart type:goal chartMajor label:Revenue 2020 YTD Minor label:(thousand USD) Minimum:0Maximum:300Target:260Value:170',
);
});
it('should correctly render ascending semantic values', () => {
Expand Down
2 changes: 0 additions & 2 deletions packages/charts/src/components/accessibility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,3 @@

/* @internal */
export { ScreenReaderSummary } from './screen_reader_summary';
export { ScreenReaderPartitionTable } from './partitions_data_table';
export { GoalSemanticDescription } from './goal_semantic_description';
44 changes: 0 additions & 44 deletions packages/charts/src/components/accessibility/label.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';

import type { ScreenReaderItem } from '../../state/chart_selectors';
import type { A11ySettings } from '../../state/selectors/get_accessibility_config';

interface ScreenReaderItemsProps {
screenReaderItems?: ScreenReaderItem[];
}

/** @internal */
export function ScreenReaderItems({ screenReaderItems }: A11ySettings & ScreenReaderItemsProps) {
const hasScreenReaderItems = screenReaderItems && screenReaderItems.length > 0;

if (!hasScreenReaderItems) {
return null;
}

return (
<dl>
{screenReaderItems.map((part) => (
<React.Fragment key={part.id || part.label}>
<dt>{part.label}:</dt>
<dd id={part.id}>{part.value}</dd>
</React.Fragment>
))}
</dl>
);
}
Loading