diff --git a/packages/osd-charts/api/charts.api.md b/packages/osd-charts/api/charts.api.md index 3797a95ec77..42e3f5ad72d 100644 --- a/packages/osd-charts/api/charts.api.md +++ b/packages/osd-charts/api/charts.api.md @@ -621,7 +621,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' | 'description' | 'useDefaultSummary'; +export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'debug' | 'tooltip' | 'theme' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' | 'externalPointerEvents' | 'showLegend' | 'showLegendExtra' | 'legendPosition' | 'legendMaxDepth' | 'ariaUseDefaultSummary' | 'ariaLabelHeadingLevel'; // @public (undocumented) export const DEPTH_KEY = "depth"; @@ -1738,12 +1738,17 @@ export interface SettingsSpec extends Spec, LegendSpec { allowBrushingLastHistogramBucket?: boolean; // (undocumented) animateData: boolean; + ariaDescribedBy?: string; + ariaDescription?: string; + ariaLabel?: string; + ariaLabelHeadingLevel: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'; + ariaLabelledBy?: string; + ariaUseDefaultSummary: boolean; baseTheme?: Theme; brushAxis?: BrushAxis; debug: boolean; // @alpha debugState?: boolean; - description?: string; // @alpha externalPointerEvents: ExternalPointerEventsSettings; hideDuplicateAxes: boolean; @@ -1774,7 +1779,6 @@ export interface SettingsSpec extends Spec, LegendSpec { roundHistogramBrushValues?: boolean; theme?: PartialTheme | PartialTheme[]; tooltip: TooltipSettings; - useDefaultSummary: boolean; // (undocumented) xDomain?: CustomXDomain; } diff --git a/packages/osd-charts/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/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-test-cases-accessibility-customizations-visually-looks-correct-1-snap.png similarity index 100% rename from packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-test-cases-add-custom-description-visually-looks-correct-1-snap.png rename to packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-test-cases-accessibility-customizations-visually-looks-correct-1-snap.png diff --git a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index e9b35ea5db2..ee1aba5f034 100644 --- a/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/packages/osd-charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -22,10 +22,16 @@ import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { LegendItem } from '../../../../common/legend'; +import { Description } from '../../../../components/accessibility/description'; +import { Label } from '../../../../components/accessibility/label'; import { onChartRendered } from '../../../../state/actions/chart'; import { GlobalChartState } from '../../../../state/chart_state'; +import { + A11ySettings, + DEFAULT_A11_SETTINGS, + getA11ySettingsSelector, +} from '../../../../state/selectors/get_accessibility_config'; 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'; @@ -79,9 +85,7 @@ export interface ReactiveChartStateProps { annotationSpecs: AnnotationSpec[]; panelGeoms: PanelGeoms; seriesTypes: Set; - description?: string; - useDefaultSummary: boolean; - chartId: string; + a11ySettings: A11ySettings; } interface ReactiveChartDispatchProps { @@ -159,9 +163,7 @@ class XYChartComponent extends React.Component { isChartEmpty, chartContainerDimensions: { width, height }, seriesTypes, - description, - useDefaultSummary, - chartId, + a11ySettings, } = this.props; if (!initialized || isChartEmpty) { @@ -171,9 +173,9 @@ 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 } : {})} > - {(description || useDefaultSummary) && ( -
- {description &&

{description}

} - {useDefaultSummary && ( -
-
Chart type
-
{chartSeriesTypes}
-
- )} -
- )} +
+
); @@ -252,9 +252,7 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { annotationSpecs: [], panelGeoms: [], seriesTypes: new Set(), - description: undefined, - useDefaultSummary: true, - chartId: '', + a11ySettings: DEFAULT_A11_SETTINGS, }; const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { @@ -263,7 +261,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { } const { geometries, geometriesIndex } = computeSeriesGeometriesSelector(state); - const { debug, description, useDefaultSummary } = getSettingsSpecSelector(state); + const { debug } = getSettingsSpecSelector(state); return { initialized: true, @@ -285,9 +283,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { annotationSpecs: getAnnotationSpecsSelector(state), panelGeoms: computePanelsSelectors(state), seriesTypes: getSeriesTypes(state), - description, - useDefaultSummary, - chartId: getChartIdSelector(state), + a11ySettings: getA11ySettingsSelector(state), }; }; diff --git a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.a11y.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.a11y.test.ts deleted file mode 100644 index cc296f7c06f..00000000000 --- a/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.a11y.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.accessibility.test.ts b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.accessibility.test.ts new file mode 100644 index 00000000000..b07e9fbf63c --- /dev/null +++ b/packages/osd-charts/src/chart_types/xy_chart/state/chart_state.accessibility.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { DEFAULT_A11_SETTINGS } from '../../../state/selectors/get_accessibility_config'; +import { getSettingsSpecSelector } from '../../../state/selectors/get_settings_specs'; + +describe('test accessibility prop defaults', () => { + 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 { + ariaDescription, + ariaUseDefaultSummary, + ariaLabelHeadingLevel, + ariaLabel, + ariaLabelledBy, + } = getSettingsSpecSelector(state); + expect(ariaDescription).toBeUndefined(); + expect(ariaUseDefaultSummary).toBeTrue(); + expect(ariaLabelHeadingLevel).toBe(DEFAULT_A11_SETTINGS.labelHeadingLevel); + expect(ariaLabel).toBeUndefined(); + expect(ariaLabelledBy).toBeUndefined(); + }); +}); +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 allow user to set a custom description for chart', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + ariaDescription: 'This is sample Kibana data', + }), + ], + store, + ); + const state = store.getState(); + const { ariaDescription } = getSettingsSpecSelector(state); + expect(ariaDescription).toBe('This is sample Kibana data'); + }); + it('should be able to disable generated descriptions', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + ariaUseDefaultSummary: false, + }), + ], + store, + ); + const state = store.getState(); + const { ariaUseDefaultSummary } = getSettingsSpecSelector(state); + expect(ariaUseDefaultSummary).toBe(false); + }); +}); +describe('custom labels 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 allow label set by the user', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + ariaLabel: 'Label set by user', + }), + ], + store, + ); + const state = store.getState(); + const { ariaLabel } = getSettingsSpecSelector(state); + expect(ariaLabel).toBe('Label set by user'); + }); + it('should allow labelledBy set by the user', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + ariaLabelledBy: 'label-id', + }), + ], + store, + ); + const state = store.getState(); + const { ariaLabelledBy } = getSettingsSpecSelector(state); + expect(ariaLabelledBy).toBe('label-id'); + }); + it('should allow users to specify valid heading levels', () => { + MockStore.addSpecs( + [ + MockGlobalSpec.settings({ + ariaLabelHeadingLevel: 'h5', + }), + ], + store, + ); + const state = store.getState(); + const { ariaLabelHeadingLevel } = getSettingsSpecSelector(state); + expect(ariaLabelHeadingLevel).toBe('h5'); + }); +}); diff --git a/packages/osd-charts/src/components/__snapshots__/chart.test.tsx.snap b/packages/osd-charts/src/components/__snapshots__/chart.test.tsx.snap index 995341dbc10..c0cbfa476b2 100644 --- a/packages/osd-charts/src/components/__snapshots__/chart.test.tsx.snap +++ b/packages/osd-charts/src/components/__snapshots__/chart.test.tsx.snap @@ -54,7 +54,7 @@ exports[`Chart should render the legend name test 1`] = ` - + @@ -72,11 +72,13 @@ exports[`Chart should render the legend name test 1`] = ` - -
+ +
-
+