diff --git a/api/charts.api.md b/api/charts.api.md index 82deef6b39..ae3330e87f 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -617,7 +617,7 @@ export const DEFAULT_TOOLTIP_SNAP = true; export const DEFAULT_TOOLTIP_TYPE: "vertical"; // @public (undocumented) -export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'debug' | 'tooltip' | 'theme' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' | 'externalPointerEvents' | 'showLegend' | 'showLegendExtra' | 'legendPosition' | 'legendMaxDepth'; +export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'debug' | 'tooltip' | 'theme' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' | 'externalPointerEvents' | 'showLegend' | 'showLegendExtra' | 'legendPosition' | 'legendMaxDepth' | 'description' | 'useDefaultSummary'; // @public (undocumented) export const DEPTH_KEY = "depth"; @@ -1739,6 +1739,7 @@ export interface SettingsSpec extends Spec, LegendSpec { debug: boolean; // @alpha debugState?: boolean; + description?: string; // @alpha externalPointerEvents: ExternalPointerEventsSettings; hideDuplicateAxes: boolean; @@ -1769,6 +1770,7 @@ export interface SettingsSpec extends Spec, LegendSpec { roundHistogramBrushValues?: boolean; theme?: PartialTheme | PartialTheme[]; tooltip: TooltipSettings; + useDefaultSummary: boolean; // (undocumented) xDomain?: CustomXDomain; } diff --git a/integration/page_objects/common.ts b/integration/page_objects/common.ts index 71983806e1..8db68687b7 100644 --- a/integration/page_objects/common.ts +++ b/integration/page_objects/common.ts @@ -474,6 +474,16 @@ class CommonPage { }); return accessibilitySnapshot; } + + /** + * Get HTML for element to test aria labels etc + */ + // eslint-disable-next-line class-methods-use-this + async getElementHTML(url: string) { + await this.loadElementFromURL(url); + // https://github.com/puppeteer/puppeteer/issues/406#issuecomment-323555639 + return await page.evaluate(() => new XMLSerializer().serializeToString(document)); + } } export const common = new CommonPage(); diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-test-cases-add-custom-description-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-test-cases-add-custom-description-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..4d6061f750 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-test-cases-add-custom-description-visually-looks-correct-1-snap.png differ diff --git a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index 40a174a841..e9b35ea5db 100644 --- a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -25,6 +25,7 @@ import { LegendItem } from '../../../../common/legend'; import { onChartRendered } from '../../../../state/actions/chart'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; @@ -78,6 +79,9 @@ export interface ReactiveChartStateProps { annotationSpecs: AnnotationSpec[]; panelGeoms: PanelGeoms; seriesTypes: Set; + description?: string; + useDefaultSummary: boolean; + chartId: string; } interface ReactiveChartDispatchProps { @@ -155,6 +159,9 @@ class XYChartComponent extends React.Component { isChartEmpty, chartContainerDimensions: { width, height }, seriesTypes, + description, + useDefaultSummary, + chartId, } = this.props; if (!initialized || isChartEmpty) { @@ -164,7 +171,7 @@ class XYChartComponent extends React.Component { const chartSeriesTypes = seriesTypes.size > 1 ? `Mixed chart: ${[...seriesTypes].join(' and ')} chart` : `${[...seriesTypes]} chart`; - + const chartIdDescription = `${chartId}--description`; return (
{ }} // eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role role="presentation" + {...(description ? { 'aria-describedby': chartIdDescription } : {})} > -
-
Chart type
-
{chartSeriesTypes}
-
+ {(description || useDefaultSummary) && ( +
+ {description &&

{description}

} + {useDefaultSummary && ( +
+
Chart type
+
{chartSeriesTypes}
+
+ )} +
+ )}
); @@ -237,6 +252,9 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { annotationSpecs: [], panelGeoms: [], seriesTypes: new Set(), + description: undefined, + useDefaultSummary: true, + chartId: '', }; const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { @@ -245,11 +263,12 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { } const { geometries, geometriesIndex } = computeSeriesGeometriesSelector(state); + const { debug, description, useDefaultSummary } = getSettingsSpecSelector(state); return { initialized: true, isChartEmpty: isChartEmptySelector(state), - debug: getSettingsSpecSelector(state).debug, + debug, geometries, geometriesIndex, theme: getChartThemeSelector(state), @@ -266,6 +285,9 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { annotationSpecs: getAnnotationSpecsSelector(state), panelGeoms: computePanelsSelectors(state), seriesTypes: getSeriesTypes(state), + description, + useDefaultSummary, + chartId: getChartIdSelector(state), }; }; diff --git a/src/chart_types/xy_chart/state/chart_state.a11y.test.ts b/src/chart_types/xy_chart/state/chart_state.a11y.test.ts new file mode 100644 index 0000000000..cc296f7c06 --- /dev/null +++ b/src/chart_types/xy_chart/state/chart_state.a11y.test.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Store } from 'redux'; + +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store/store'; +import { GlobalChartState } from '../../../state/chart_state'; +import { getSettingsSpecSelector } from '../../../state/selectors/get_settings_specs'; + +describe('custom description for screen readers', () => { + let store: Store; + beforeEach(() => { + store = MockStore.default(); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + data: [ + { x: 1, y: 10 }, + { x: 2, y: 5 }, + ], + }), + MockGlobalSpec.settings(), + ], + store, + ); + }); + it('should test defaults', () => { + const state = store.getState(); + const { description, useDefaultSummary } = getSettingsSpecSelector(state); + expect(description).toBeUndefined(); + expect(useDefaultSummary).toBeTrue(); + }); + it('should allow user to set a custom description for chart', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + description: 'This is sample Kibana data', + }), + ], + store, + ); + const state = store.getState(); + const { description } = getSettingsSpecSelector(state); + expect(description).toBe('This is sample Kibana data'); + }); + it('should be able to disable generated descriptions', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + useDefaultSummary: false, + }), + ], + store, + ); + const state = store.getState(); + const { useDefaultSummary } = getSettingsSpecSelector(state); + expect(useDefaultSummary).toBe(false); + }); +}); diff --git a/src/components/__snapshots__/chart.test.tsx.snap b/src/components/__snapshots__/chart.test.tsx.snap index 6c1e2f571a..995341dbc1 100644 --- a/src/components/__snapshots__/chart.test.tsx.snap +++ b/src/components/__snapshots__/chart.test.tsx.snap @@ -54,7 +54,7 @@ exports[`Chart should render the legend name test 1`] = ` - + @@ -72,17 +72,19 @@ exports[`Chart should render the legend name test 1`] = ` - +
-
-
- Chart type -
-
- bar chart -
-
+
+
+
+ Chart type +
+
+ bar chart +
+
+
diff --git a/src/specs/constants.ts b/src/specs/constants.ts index ed3f70674f..f6c7c98b19 100644 --- a/src/specs/constants.ts +++ b/src/specs/constants.ts @@ -155,6 +155,7 @@ export const DEFAULT_SETTINGS_SPEC: SettingsSpec = { baseTheme: LIGHT_THEME, brushAxis: BrushAxis.X, minBrushDelta: 2, + useDefaultSummary: true, ...DEFAULT_LEGEND_CONFIG, }; diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 5eda362448..44ac0599e7 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -548,6 +548,15 @@ export interface SettingsSpec extends Spec, LegendSpec { * Render component for no results UI */ noResults?: ComponentType | ReactChild; + /** + * User can provide a custom description to be read by a screen reader about their chart + */ + description?: string; + /** + * Disable the automated charts series types from being provided for screen readers + * @defaultValue true + */ + useDefaultSummary: boolean; } /** @@ -608,7 +617,9 @@ export type DefaultSettingsProps = | 'showLegend' | 'showLegendExtra' | 'legendPosition' - | 'legendMaxDepth'; + | 'legendMaxDepth' + | 'description' + | 'useDefaultSummary'; /** @public */ export type SettingsSpecProps = Partial>; diff --git a/stories/test_cases/6_a11y_custom_description.tsx b/stories/test_cases/6_a11y_custom_description.tsx new file mode 100644 index 0000000000..118a27b57f --- /dev/null +++ b/stories/test_cases/6_a11y_custom_description.tsx @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { boolean, text } from '@storybook/addon-knobs'; +import React from 'react'; + +import { AreaSeries, Chart, ScaleType, Settings } from '../../src'; +import { KIBANA_METRICS } from '../../src/utils/data_samples/test_dataset_kibana'; + +export const Example = () => { + const automatedSeries = boolean('Use the default generated series types of charts for screen readers', true); + const customDescriptionForScreenReaders = text('custom description for screen readers', ''); + return ( + + + + + ); +}; diff --git a/stories/test_cases/test_cases.stories.tsx b/stories/test_cases/test_cases.stories.tsx index 3f75475915..b9c2885a71 100644 --- a/stories/test_cases/test_cases.stories.tsx +++ b/stories/test_cases/test_cases.stories.tsx @@ -31,3 +31,4 @@ export { Example as chromePathBugFix } from './2_chrome_path_bug_fix'; export { Example as noAxesAnnotationBugFix } from './3_no_axes_annotation'; export { Example as filterZerosInLogFitDomain } from './4_filter_zero_values_log'; export { Example as legendScrollBarSizing } from './5_legend_scroll_bar_sizing'; +export { Example as addCustomDescription } from './6_a11y_custom_description';