Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
414edc2
feat(a11y): enhance accessibility with consolidated screen reader des…
walterra Jun 27, 2025
939ee32
refactor(a11y): remove DataTableToggle functionality
walterra Jun 27, 2025
427e33d
feat(a11y): improve time axis description granularity based on data t…
walterra Jun 27, 2025
ea2b89c
feat(a11y): include actual series count and stacking information in c…
walterra Jun 27, 2025
8492037
refactor(a11y): replace div with figcaption for better semantic struc…
walterra Jun 30, 2025
383e8a5
refactor: split screen_reader_summary into focused utility modules
walterra Jun 30, 2025
7d67d76
test: add unit tests for accessibility summary utility functions
walterra Jun 30, 2025
6795e0a
fix: update accessibility tests to match refactored utility output fo…
walterra Jun 30, 2025
f06805f
add bar chart accessibility tests using existing mock data
walterra Jun 30, 2025
5411981
feat(a11y): add dynamic screen reader summaries for chart accessibility
walterra Jun 30, 2025
a148396
test(a11y): improve bar chart accessibility tests to assert full summ…
walterra Jun 30, 2025
73bd9e9
test: simplify bar chart accessibility test to use basic single serie…
walterra Jun 30, 2025
53a4718
feat(a11y): enhance single series bar chart descriptions with data co…
walterra Jun 30, 2025
47dd81a
fix(a11y): capitalize chart type descriptions for better screen reade…
walterra Jun 30, 2025
f460c01
refactor: consolidate capitalizeFirst utility to shared text utils
walterra Jun 30, 2025
309ad43
refactor(a11y): consolidate screen reader logic into centralized sele…
walterra Jun 30, 2025
dd5878e
test: update bar chart accessibility test expectations
walterra Jul 2, 2025
e42c74b
refactor(a11y): implement chart-specific screen reader selectors foll…
walterra Jul 2, 2025
a471b7e
refactor(a11y): relocate goal chart summary utils to chart type direc…
walterra Jul 3, 2025
2ae2af2
fix(a11y): improve accessibility summary grammar for singular/plural …
walterra Jul 3, 2025
0ad1ce7
refactor(a11y): move chart type descriptions to chart-specific screen…
walterra Jul 3, 2025
8999720
refactor(a11y): rename PartitionData.data to sections to avoid nested…
walterra Jul 3, 2025
964efb0
refactor(a11y): make ScreenReaderLabel component chart-agnostic by in…
walterra Jul 3, 2025
ce4d0a7
test(a11y): break out accessibility tests into separate files for eac…
walterra Jul 3, 2025
a01f6bc
refactor(a11y): move XY chart axis accessibility components to chart-…
walterra Jul 3, 2025
9c2e908
refactor(a11y): remove redundant AxisDescriptions React component in …
walterra Jul 3, 2025
8d437a3
refactor(a11y): consolidate accessibility descriptions into A11ySetti…
walterra Jul 7, 2025
1bdc0d3
refactor(a11y): consolidate description rendering in ScreenReaderDesc…
walterra Jul 7, 2025
5458032
refactor(a11y): simplify description rendering and combine custom/gen…
walterra Jul 7, 2025
a292fd4
feat(a11y): add comprehensive accessibility testing infrastructure wi…
walterra Jul 7, 2025
60f0f3b
refactor(e2e): reorganize accessibility tests into separate files by …
walterra Jul 7, 2025
dd5e4da
fix(e2e): update accessibility test URLs to use valid Storybook story…
walterra Jul 7, 2025
90abfa2
test(a11y): replace TODO placeholders with actual expected accessibil…
walterra Jul 8, 2025
8f3550d
fix(a11y): move screen reader elements outside canvas and add specifi…
walterra Jul 8, 2025
f7a0b78
refactor(e2e): remove accessibility helper file and clean up merge co…
walterra Aug 11, 2025
77913df
refactor(e2e): remove legacy accessibility test scripts
walterra Aug 11, 2025
4800276
remove chart-specific a11y test files
walterra Aug 11, 2025
c44c133
cleanup
walterra Sep 15, 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
1 change: 1 addition & 0 deletions e2e/playwright.a11y.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import baseConfig from './playwright.config';

const config: PlaywrightTestConfig = {
...baseConfig,
testIgnore: undefined, // Reset the testIgnore from base config
testDir: 'tests_a11y',
testMatch: ['**/tests_a11y/**/*.test.ts'],
reporter: [
Expand Down
1 change: 1 addition & 0 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ expect.extend(pwExpect.matchers);
const isCI = process.env.CI === 'true';

const config: PlaywrightTestConfig = {
testIgnore: ['**/accessibility_descriptions.test.ts'],
use: {
headless: true,
locale: 'en-us',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

import { getGoalSpecSelector } from './get_goal_spec';
import { createCustomCachedSelector } from '../../../../state/create_selector';
import { capitalizeFirst } from '../../../../utils/text/text_utils';

/** @internal */
export const getChartTypeDescriptionSelector = createCustomCachedSelector([getGoalSpecSelector], (spec) => {
return `${spec?.subtype ?? 'goal'} chart`;
return `${capitalizeFirst(spec?.subtype ?? 'goal')} chart`;
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,17 @@
* Side Public License, v 1.
*/

import { getChartTypeDescriptionSelector } from './get_chart_type_description';
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 => {
[getGoalChartDataSelector, getGoalChartLabelsSelector, getChartTypeDescriptionSelector],
(goalChartData, goalChartLabels, chartTypeDescription): 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) {
Expand Down Expand Up @@ -69,6 +51,16 @@ export const getScreenReaderDataSelector = createCustomCachedSelector(
);
}

return { screenReaderItems };
const summaryParts: string[] = [];

// Add chart type description first
if (chartTypeDescription) {
summaryParts.push(chartTypeDescription);
}

return {
summaryParts,
screenReaderItems,
};
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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 { createGoalChartDescription } from './summary_utils';
import { ChartType } from '../../../chart_types';
import type { GoalChartData } from '../state/selectors/get_goal_chart_data';

describe('createGoalChartDescription', () => {
const mockGoalChartData: GoalChartData = {
minimum: 0,
maximum: 300,
target: 260,
value: 170,
};

it('should return null for non-goal chart types', () => {
const result = createGoalChartDescription(ChartType.XYAxis, mockGoalChartData);
expect(result).toBeNull();
});

it('should return null when goalChartData is undefined', () => {
const result = createGoalChartDescription(ChartType.Goal, undefined);
expect(result).toBeNull();
});

it('should return null when maximum is NaN', () => {
const invalidData = {
...mockGoalChartData,
maximum: NaN,
};
const result = createGoalChartDescription(ChartType.Goal, invalidData);
expect(result).toBeNull();
});

it('should create description for goal chart', () => {
const result = createGoalChartDescription(ChartType.Goal, mockGoalChartData);
expect(result).toBe('Minimum: 0, Maximum: 300, Target: 260, Value: 170');
});

it('should create description for bullet chart', () => {
const result = createGoalChartDescription(ChartType.Bullet, mockGoalChartData);
expect(result).toBe('Minimum: 0, Maximum: 300, Target: 260, Value: 170');
});

it('should return null for null chart type', () => {
const result = createGoalChartDescription(null, mockGoalChartData);
expect(result).toBeNull();
});

it('should handle decimal values', () => {
const decimalData: GoalChartData = {
minimum: 0.5,
maximum: 100.75,
target: 85.25,
value: 67.8,
};
const result = createGoalChartDescription(ChartType.Goal, decimalData);
expect(result).toBe('Minimum: 0.5, Maximum: 100.75, Target: 85.25, Value: 67.8');
});

it('should handle negative values', () => {
const negativeData: GoalChartData = {
minimum: -50,
maximum: 50,
target: 10,
value: -5,
};
const result = createGoalChartDescription(ChartType.Goal, negativeData);
expect(result).toBe('Minimum: -50, Maximum: 50, Target: 10, Value: -5');
});

it('should handle zero values', () => {
const zeroData: GoalChartData = {
minimum: 0,
maximum: 0,
target: 0,
value: 0,
};
const result = createGoalChartDescription(ChartType.Goal, zeroData);
expect(result).toBe('Minimum: 0, Maximum: 0, Target: 0, Value: 0');
});

it('should handle large numbers', () => {
const largeData: GoalChartData = {
minimum: 1000000,
maximum: 5000000,
target: 4000000,
value: 3500000,
};
const result = createGoalChartDescription(ChartType.Goal, largeData);
expect(result).toBe('Minimum: 1000000, Maximum: 5000000, Target: 4000000, Value: 3500000');
});
});
21 changes: 21 additions & 0 deletions packages/charts/src/chart_types/goal_chart/utils/summary_utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 { ChartType } from '../../../chart_types';
import type { GoalChartData } from '../state/selectors/get_goal_chart_data';

/** @internal */
export function createGoalChartDescription(chartType: ChartType | null, goalChartData?: GoalChartData): string | null {
const isGoalChart = chartType === ChartType.Goal || chartType === ChartType.Bullet;

if (!isGoalChart || !goalChartData || isNaN(goalChartData.maximum)) {
return null;
}

return `Minimum: ${goalChartData.minimum}, Maximum: ${goalChartData.maximum}, Target: ${goalChartData.target}, Value: ${goalChartData.value}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getPointerCursorSelector } from './selectors/get_cursor_pointer';
import { getDebugStateSelector } from './selectors/get_debug_state';
import { getHeatmapTableSelector } from './selectors/get_heatmap_table';
import { getLegendItemsLabelsSelector } from './selectors/get_legend_items_labels';
import { getScreenReaderDataSelector } from './selectors/get_screen_reader_data';
import { getTooltipAnchorSelector } from './selectors/get_tooltip_anchor';
import { isBrushAvailableSelector } from './selectors/is_brush_available';
import { isEmptySelector } from './selectors/is_empty';
Expand Down Expand Up @@ -48,6 +49,7 @@ export const chartSelectorsFactory = createChartSelectorsFactory(
getBrushArea: getBrushAreaSelector,
getDebugState: getDebugStateSelector,
getChartTypeDescription: () => 'Heatmap chart',
getScreenReaderData: getScreenReaderDataSelector,
getSmallMultiplesDomains: getHeatmapTableSelector,
},
// event callbacks
Expand Down
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 { getHeatmapSpecSelector } from './get_heatmap_spec';
import type { ChartSpecificScreenReaderData } from '../../../../state/chart_selectors';
import { createCustomCachedSelector } from '../../../../state/create_selector';

/** @internal */
export interface HeatmapScreenReaderData {
heatmapSpec: ReturnType<typeof getHeatmapSpecSelector>;
}

/** @internal */
export const getScreenReaderDataSelector = createCustomCachedSelector(
[getHeatmapSpecSelector],
(heatmapSpec): ChartSpecificScreenReaderData => {
const summaryParts: string[] = [];

// Add heatmap-specific accessibility information
if (heatmapSpec?.data && heatmapSpec.data.length > 0) {
summaryParts.push(`${heatmapSpec.data.length} data ${heatmapSpec.data.length === 1 ? 'point' : 'points'}`);
}

return {
data: {
heatmapSpec,
} as HeatmapScreenReaderData,
summaryParts,
};
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from '../../../../stat
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';
import { getPartitionScreenReaderDataSelector } from '../../state/selectors/get_screen_reader_data';

interface ScreenReaderPartitionTableProps {
a11ySettings: A11ySettings;
Expand Down Expand Up @@ -46,8 +46,8 @@ const ScreenReaderPartitionTableComponent = ({
}
};

const { isSmallMultiple, data, hasMultipleLayers } = partitionData;
const tableLength = data.length;
const { isSmallMultiple, sections, hasMultipleLayers } = partitionData;
const tableLength = sections.length;
const showMoreRows = rowLimit < tableLength;
let countOfCol: number = 3;
const totalColumns: number =
Expand Down Expand Up @@ -84,7 +84,7 @@ const ScreenReaderPartitionTableComponent = ({
</thead>

<tbody>
{partitionData.data
{partitionData.sections
.slice(0, rowLimit)
.map(({ panelTitle, depth, label, parentName, valueText, percentage }, index) => {
return (
Expand Down Expand Up @@ -120,7 +120,7 @@ const DEFAULT_SCREEN_READER_SUMMARY = {
partitionData: {
isSmallMultiple: false,
hasMultipleLayers: false,
data: [],
sections: [],
},
debug: false,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getChartTypeDescriptionSelector } from './selectors/get_chart_type_desc
import { getPointerCursorSelector } from './selectors/get_cursor_pointer';
import { getDebugStateSelector } from './selectors/get_debug_state';
import { getLegendItemsLabels } from './selectors/get_legend_items_labels';
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 @@ -58,6 +59,7 @@ export const chartSelectorsFactory = createChartSelectorsFactory(

getDebugState: getDebugStateSelector,
getChartTypeDescription: getChartTypeDescriptionSelector,
getScreenReaderData: getScreenReaderDataSelector,
},
// event callbacks
[createOnElementClickCaller, createOnElementOverCaller, createOnElementOutCaller],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

import { getPartitionSpec } from './partition_spec';
import { createCustomCachedSelector } from '../../../../state/create_selector';
import { capitalizeFirst } from '../../../../utils/text/text_utils';

/** @internal */
export const getChartTypeDescriptionSelector = createCustomCachedSelector(
[getPartitionSpec],
(partitionSpec): string => {
return `${partitionSpec?.layout} chart` ?? 'Partition chart';
return partitionSpec?.layout ? `${capitalizeFirst(partitionSpec.layout)} chart` : 'Partition chart';
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import type { Store } from 'redux';

import { getPartitionScreenReaderDataSelector } from './get_screen_reader_data';
import { getScreenReaderDataSelector } 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,27 +65,33 @@ describe('Get screen reader data', () => {

it('should test defaults', () => {
MockStore.addSpecs([spec1], store);
const expected = getPartitionScreenReaderDataSelector(store.getState());
const expected = getScreenReaderDataSelector(store.getState());
expect(expected).toEqual({
data: [
{ depth: 1, label: 'aaa', panelTitle: '', parentName: 'none', percentage: '100%', value: 3, valueText: '3' },
{ depth: 2, label: 'aa', panelTitle: '', parentName: 'aaa', percentage: '67%', value: 2, valueText: '2' },
{ depth: 3, label: '1', panelTitle: '', parentName: 'aa', percentage: '33%', value: 1, valueText: '1' },
{ depth: 3, label: '3', panelTitle: '', parentName: 'aa', percentage: '33%', value: 1, valueText: '1' },
{ depth: 2, label: 'bb', panelTitle: '', parentName: 'aaa', percentage: '33%', value: 1, valueText: '1' },
{ depth: 3, label: '4', panelTitle: '', parentName: 'bb', percentage: '33%', value: 1, valueText: '1' },
],
hasMultipleLayers: true,
isSmallMultiple: false,
data: {
sections: [
{ depth: 1, label: 'aaa', panelTitle: '', parentName: 'none', percentage: '100%', value: 3, valueText: '3' },
{ depth: 2, label: 'aa', panelTitle: '', parentName: 'aaa', percentage: '67%', value: 2, valueText: '2' },
{ depth: 3, label: '1', panelTitle: '', parentName: 'aa', percentage: '33%', value: 1, valueText: '1' },
{ depth: 3, label: '3', panelTitle: '', parentName: 'aa', percentage: '33%', value: 1, valueText: '1' },
{ depth: 2, label: 'bb', panelTitle: '', parentName: 'aaa', percentage: '33%', value: 1, valueText: '1' },
{ depth: 3, label: '4', panelTitle: '', parentName: 'bb', percentage: '33%', value: 1, valueText: '1' },
],
hasMultipleLayers: true,
isSmallMultiple: false,
},
summaryParts: ['Sunburst chart', '6 data points', 'with hierarchical layers'],
});
});
it('should compute screen reader data for no slices in pie', () => {
MockStore.addSpecs([specNoSlice], store);
const expected = getPartitionScreenReaderDataSelector(store.getState());
const expected = getScreenReaderDataSelector(store.getState());
expect(expected).toEqual({
data: [],
hasMultipleLayers: true,
isSmallMultiple: false,
data: {
sections: [],
hasMultipleLayers: true,
isSmallMultiple: false,
},
summaryParts: ['Sunburst chart'],
});
});
});
Loading