diff --git a/e2e/tests_a11y/goal_chart_a11y.test.ts b/e2e/tests_a11y/goal_chart_a11y.test.ts index f1609fa83f..6b4ea10fd3 100644 --- a/e2e/tests_a11y/goal_chart_a11y.test.ts +++ b/e2e/tests_a11y/goal_chart_a11y.test.ts @@ -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', ); }); diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index c4ba638c83..2f61db1a43 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -246,7 +246,7 @@ export interface ArrayNode extends NodeDescriptor { } // @public -export const Axis: FC>; +export const Axis: FC>; // @public (undocumented) export type AxisId = string; @@ -3201,7 +3201,7 @@ export type TooltipAction, "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, "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; diff --git a/packages/charts/src/components/accessibility/goal_semantic_description.tsx b/packages/charts/src/chart_types/goal_chart/components/goal_semantic_description.tsx similarity index 87% rename from packages/charts/src/components/accessibility/goal_semantic_description.tsx rename to packages/charts/src/chart_types/goal_chart/components/goal_semantic_description.tsx index e112029b72..7acf9346a3 100644 --- a/packages/charts/src/components/accessibility/goal_semantic_description.tsx +++ b/packages/charts/src/chart_types/goal_chart/components/goal_semantic_description.tsx @@ -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[]; diff --git a/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx b/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx index 9444b350f2..797fd27eb3 100644 --- a/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx +++ b/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx @@ -16,7 +16,7 @@ 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'; @@ -24,6 +24,7 @@ import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from '../../../../stat 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'; @@ -138,10 +139,9 @@ class Component extends React.Component { }} // eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role role="presentation" - > - - - + /> + + ); } diff --git a/packages/charts/src/chart_types/goal_chart/state/chart_selectors.ts b/packages/charts/src/chart_types/goal_chart/state/chart_selectors.ts index ae0975746b..3f376d2ccb 100644 --- a/packages/charts/src/chart_types/goal_chart/state/chart_selectors.ts +++ b/packages/charts/src/chart_types/goal_chart/state/chart_selectors.ts @@ -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'; @@ -46,6 +47,7 @@ export const chartSelectorsFactory = createChartSelectorsFactory( }, getChartTypeDescription: getChartTypeDescriptionSelector, + getScreenReaderData: getScreenReaderDataSelector, // TODO enable for small multiples canDisplayChartTitles: () => false, diff --git a/packages/charts/src/chart_types/goal_chart/state/selectors/get_screen_reader_data.ts b/packages/charts/src/chart_types/goal_chart/state/selectors/get_screen_reader_data.ts new file mode 100644 index 0000000000..1ac8f3322d --- /dev/null +++ b/packages/charts/src/chart_types/goal_chart/state/selectors/get_screen_reader_data.ts @@ -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 }; + }, +); diff --git a/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx b/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx index 75ec32200e..2efa935378 100644 --- a/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx +++ b/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx @@ -136,9 +136,8 @@ class Component extends React.Component { }} // eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role role="presentation" - > - - + /> + ); } 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 e2ead20c36..08bb92422b 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 @@ -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'; @@ -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 { diff --git a/packages/charts/src/components/accessibility/partitions_data_table.tsx b/packages/charts/src/chart_types/partition_chart/renderer/dom/screen_reader_partition_table.tsx similarity index 85% rename from packages/charts/src/components/accessibility/partitions_data_table.tsx rename to packages/charts/src/chart_types/partition_chart/renderer/dom/screen_reader_partition_table.tsx index fdc2057aa3..c5bbd25c07 100644 --- a/packages/charts/src/components/accessibility/partitions_data_table.tsx +++ b/packages/charts/src/chart_types/partition_chart/renderer/dom/screen_reader_partition_table.tsx @@ -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; @@ -131,7 +131,7 @@ const mapStateToProps = (state: GlobalChartState): ScreenReaderPartitionTablePro } return { a11ySettings: getA11ySettingsSelector(state), - partitionData: getScreenReaderDataSelector(state), + partitionData: getPartitionScreenReaderDataSelector(state), debug: getSettingsSpecSelector(state).debug, }; }; diff --git a/packages/charts/src/chart_types/partition_chart/state/selectors/get_screen_reader_data.test.ts b/packages/charts/src/chart_types/partition_chart/state/selectors/get_screen_reader_data.test.ts index 9d21f5e588..5269d46f62 100644 --- a/packages/charts/src/chart_types/partition_chart/state/selectors/get_screen_reader_data.test.ts +++ b/packages/charts/src/chart_types/partition_chart/state/selectors/get_screen_reader_data.test.ts @@ -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'; @@ -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' }, @@ -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, diff --git a/packages/charts/src/chart_types/partition_chart/state/selectors/get_screen_reader_data.ts b/packages/charts/src/chart_types/partition_chart/state/selectors/get_screen_reader_data.ts index 8e6c946a65..a359b16fb5 100644 --- a/packages/charts/src/chart_types/partition_chart/state/selectors/get_screen_reader_data.ts +++ b/packages/charts/src/chart_types/partition_chart/state/selectors/get_screen_reader_data.ts @@ -59,7 +59,7 @@ const getScreenReaderDataForPartitions = ( }; /** @internal */ -export const getScreenReaderDataSelector = createCustomCachedSelector( +export const getPartitionScreenReaderDataSelector = createCustomCachedSelector( [getPartitionSpecs, partitionMultiGeometries], (specs, shapeViewModel): PartitionData => { if (specs.length === 0) { diff --git a/packages/charts/src/components/accessibility/accessibility.test.tsx b/packages/charts/src/components/accessibility/accessibility.test.tsx index 920b3622bd..c10e611436 100644 --- a/packages/charts/src/components/accessibility/accessibility.test.tsx +++ b/packages/charts/src/components/accessibility/accessibility.test.tsx @@ -165,7 +165,7 @@ describe('Accessibility', () => { , ); 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', () => { diff --git a/packages/charts/src/components/accessibility/index.ts b/packages/charts/src/components/accessibility/index.ts index 2201cb17d3..df4e4497fc 100644 --- a/packages/charts/src/components/accessibility/index.ts +++ b/packages/charts/src/components/accessibility/index.ts @@ -8,5 +8,3 @@ /* @internal */ export { ScreenReaderSummary } from './screen_reader_summary'; -export { ScreenReaderPartitionTable } from './partitions_data_table'; -export { GoalSemanticDescription } from './goal_semantic_description'; diff --git a/packages/charts/src/components/accessibility/label.tsx b/packages/charts/src/components/accessibility/label.tsx deleted file mode 100644 index a331125edf..0000000000 --- a/packages/charts/src/components/accessibility/label.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { GoalChartLabels } from '../../chart_types/goal_chart/state/selectors/get_goal_chart_data'; -import type { A11ySettings } from '../../state/selectors/get_accessibility_config'; - -interface ScreenReaderLabelProps { - goalChartLabels?: GoalChartLabels; -} - -/** @internal */ -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/description.tsx b/packages/charts/src/components/accessibility/screen_reader_description.tsx similarity index 100% rename from packages/charts/src/components/accessibility/description.tsx rename to packages/charts/src/components/accessibility/screen_reader_description.tsx diff --git a/packages/charts/src/components/accessibility/screen_reader_items.tsx b/packages/charts/src/components/accessibility/screen_reader_items.tsx new file mode 100644 index 0000000000..07217a06b0 --- /dev/null +++ b/packages/charts/src/components/accessibility/screen_reader_items.tsx @@ -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 ( +
+ {screenReaderItems.map((part) => ( + +
{part.label}:
+
{part.value}
+
+ ))} +
+ ); +} diff --git a/packages/charts/src/components/accessibility/screen_reader_summary.tsx b/packages/charts/src/components/accessibility/screen_reader_summary.tsx index 4364a16c0c..93dc2bd2ac 100644 --- a/packages/charts/src/components/accessibility/screen_reader_summary.tsx +++ b/packages/charts/src/components/accessibility/screen_reader_summary.tsx @@ -9,59 +9,28 @@ import React, { memo } from 'react'; import { connect } from 'react-redux'; -import { ScreenReaderDescription } from './description'; -import { ScreenReaderLabel } from './label'; -import { ScreenReaderTypes } from './types'; -import type { GoalChartData, GoalChartLabels } from '../../chart_types/goal_chart/state/selectors/get_goal_chart_data'; -import { - getGoalChartDataSelector, - getGoalChartLabelsSelector, -} from '../../chart_types/goal_chart/state/selectors/get_goal_chart_data'; +import { ScreenReaderDescription } from './screen_reader_description'; +import { ScreenReaderItems } from './screen_reader_items'; 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 { getInternalChartStateSelector } from '../../state/selectors/get_internal_chart_state'; -import { getInternalIsInitializedSelector, InitStatus } from '../../state/selectors/get_internal_is_intialized'; +import type { ScreenReaderSummaryData } from '../../state/selectors/get_screen_reader_summary'; +import { getScreenReaderSummarySelector } from '../../state/selectors/get_screen_reader_summary'; -interface ScreenReaderSummaryStateProps { - a11ySettings: A11ySettings; - chartTypeDescription: string; - goalChartData?: GoalChartData; - goalChartLabels?: GoalChartLabels; -} - -const ScreenReaderSummaryComponent = ({ - a11ySettings, - chartTypeDescription, - goalChartData, - goalChartLabels, -}: ScreenReaderSummaryStateProps) => { +const ScreenReaderSummaryComponent = ({ a11ySettings, screenReaderData }: ScreenReaderSummaryData) => { return ( -
- +
- -
+ + ); }; -const DEFAULT_SCREEN_READER_SUMMARY = { - a11ySettings: DEFAULT_A11Y_SETTINGS, - chartTypeDescription: '', - goalChartData: undefined, -}; - -const mapStateToProps = (state: GlobalChartState): ScreenReaderSummaryStateProps => { - const internalChartState = getInternalChartStateSelector(state); - if (internalChartState === null || getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { - return DEFAULT_SCREEN_READER_SUMMARY; - } - return { - chartTypeDescription: internalChartState.getChartTypeDescription(state), - a11ySettings: getA11ySettingsSelector(state), - goalChartData: getGoalChartDataSelector(state), - goalChartLabels: getGoalChartLabelsSelector(state), - }; +/** @internal */ +export const mapStateToProps = (state: GlobalChartState): ScreenReaderSummaryData => { + return getScreenReaderSummarySelector(state); }; /** @internal */ diff --git a/packages/charts/src/components/accessibility/types.tsx b/packages/charts/src/components/accessibility/types.tsx deleted file mode 100644 index 4957a0cf07..0000000000 --- a/packages/charts/src/components/accessibility/types.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { GoalChartData } from '../../chart_types/goal_chart/state/selectors/get_goal_chart_data'; -import type { A11ySettings } from '../../state/selectors/get_accessibility_config'; - -interface ScreenReaderTypesProps { - chartTypeDescription: string; - goalChartData?: GoalChartData; -} - -/** @internal */ -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:
-
{chartTypeDescription}
- {validGoalChart && goalChartData && !isNaN(goalChartData.maximum) ? ( - <> -
Minimum:
-
{goalChartData.minimum}
-
Maximum:
-
{goalChartData.maximum}
-
Target:
-
{goalChartData.target}
-
Value:
-
{goalChartData.value}
- - ) : null} -
- ); -} diff --git a/packages/charts/src/state/chart_selectors.ts b/packages/charts/src/state/chart_selectors.ts index 5c07a54355..95a5a7c2a8 100644 --- a/packages/charts/src/state/chart_selectors.ts +++ b/packages/charts/src/state/chart_selectors.ts @@ -19,8 +19,25 @@ import type { SmallMultiplesSeriesDomains } from '../common/panel_utils'; import type { SeriesKey } from '../common/series_id'; import type { AnchorPosition } from '../components/portal/types'; import type { TooltipInfo } from '../components/tooltip/types'; +import { getA11ySettingsSelector } from '../state/selectors/get_accessibility_config'; import type { Dimensions } from '../utils/dimensions'; +/** @internal */ +export interface ScreenReaderItem { + /** The label for this part of the summary */ + label: string; + /** Optional ID for referencing this part */ + id?: string; + /** The value for this part of the summary */ + value: string; +} + +/** @internal */ +export interface ChartSpecificScreenReaderData { + /** Custom summary parts to include in the consolidated summary */ + screenReaderItems?: ScreenReaderItem[]; +} + /** @internal */ export interface LegendItemLabel { label: string; @@ -137,6 +154,11 @@ export interface ChartSelectors { */ getChartTypeDescription(globalState: GlobalChartState): string; + /** + * Get chart-specific data for screen reader accessibility + */ + getScreenReaderData?(globalState: GlobalChartState): ChartSpecificScreenReaderData; + /** * Get the domain of the vertical and horizontal small multiple grids */ @@ -164,7 +186,7 @@ export const createChartSelectorsFactory = () => { const callbacks = callbacksCreators.map((cb) => cb()); - return { + const chartSelectors = { isInitialized: () => InitStatus.SpecNotInitialized, isBrushAvailable: () => false, isBrushing: () => false, @@ -186,6 +208,16 @@ export const createChartSelectorsFactory = getBrushArea: () => null, getDebugState: () => ({}), getChartTypeDescription: () => '', + // The default screen reader data returns just the chart type description. + getScreenReaderData: (state: GlobalChartState): ChartSpecificScreenReaderData => { + const a11ySettings = getA11ySettingsSelector(state); + const chartTypeDescription = chartSelectors.getChartTypeDescription(state); + return { + screenReaderItems: chartTypeDescription + ? [{ label: 'Chart type', id: a11ySettings.defaultSummaryId, value: chartTypeDescription }] + : [], + }; + }, getSmallMultiplesDomains: () => ({ smVDomain: [], smHDomain: [] }), canDisplayChartTitles: () => true, ...overrides, @@ -193,6 +225,8 @@ export const createChartSelectorsFactory = callbacks.forEach((cb) => cb(state)); }, }; + + return chartSelectors; }; /** @internal */ diff --git a/packages/charts/src/state/selectors/get_screen_reader_summary.ts b/packages/charts/src/state/selectors/get_screen_reader_summary.ts new file mode 100644 index 0000000000..5ec086c02e --- /dev/null +++ b/packages/charts/src/state/selectors/get_screen_reader_summary.ts @@ -0,0 +1,49 @@ +/* + * 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 type { A11ySettings } from './get_accessibility_config'; +import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from './get_accessibility_config'; +import { getInternalChartStateSelector } from './get_internal_chart_state'; +import { getInternalIsInitializedSelector, InitStatus } from './get_internal_is_intialized'; +import type { ChartSpecificScreenReaderData } from '../chart_selectors'; +import type { GlobalChartState } from '../chart_state'; +import { createCustomCachedSelector } from '../create_selector'; + +/** @internal */ +export interface ScreenReaderSummaryData { + a11ySettings: A11ySettings; + screenReaderData?: ChartSpecificScreenReaderData; +} + +const DEFAULT_SCREEN_READER_SUMMARY: ScreenReaderSummaryData = { + a11ySettings: DEFAULT_A11Y_SETTINGS, + screenReaderData: undefined, +}; + +/** @internal */ +export const getScreenReaderSummarySelector = createCustomCachedSelector( + [ + getInternalChartStateSelector, + getInternalIsInitializedSelector, + getA11ySettingsSelector, + (state: GlobalChartState) => state, + ], + (internalChartState, initStatus, a11ySettings, state) => { + if (internalChartState === null || initStatus !== InitStatus.Initialized) { + return DEFAULT_SCREEN_READER_SUMMARY; + } + + // Get chart-specific screen reader data + const screenReaderData = internalChartState.getScreenReaderData?.(state); + + return { + a11ySettings, + screenReaderData, + }; + }, +);