diff --git a/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts b/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts new file mode 100644 index 000000000000..d5a9dc3b1b02 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts @@ -0,0 +1,106 @@ +import PivotGrid from 'devextreme-testcafe-models/pivotGrid'; +import { Selector } from 'testcafe'; +import url from '../../helpers/getPageUrl'; +import { createWidget } from '../../helpers/createWidget'; +import { a11yCheck } from '../../helpers/accessibility/utils'; + +fixture.disablePageReloads`PivotGrid - Accessibility` + .page(url(__dirname, '../container.html')); + +const PIVOT_GRID_SELECTOR = '#container'; + +const createConfig = () => ({ + width: 1000, + allowSorting: true, + allowSortingBySummary: true, + allowFiltering: true, + allowExpandAll: true, + showBorders: true, + fieldPanel: { + visible: true, + }, + fieldChooser: { + enabled: true, + height: 500, + }, + dataSource: { + fields: [{ + dataField: 'country', + area: 'filter', + }, { + caption: 'Region', + width: 120, + dataField: 'region', + area: 'row', + }, { + caption: 'City', + dataField: 'city', + width: 150, + area: 'row', + }, { + caption: 'Country', + dataField: 'country', + area: 'column', + }, { + dataField: 'date', + dataType: 'date', + area: 'column', + }, { + groupName: 'date', + groupInterval: 'year', + expanded: true, + area: 'column', + }, { + caption: 'Sales', + dataField: 'amount', + dataType: 'number', + summaryType: 'sum', + area: 'data', + }], + store: [{ + region: 'Africa', + country: 'Egypt', + city: 'Cairo', + amount: 500, + date: new Date('2015-05-26'), + }, { + region: 'South America', + country: 'Argentina', + city: 'Buenos Aires', + amount: 780, + date: new Date('2015-05-07'), + }], + }, +}); + +test('grid with field panel', async (t) => { + await a11yCheck(t, {}, PIVOT_GRID_SELECTOR); +}).before(async () => createWidget('dxPivotGrid', createConfig())); + +test('field chooser popup', async (t) => { + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + + await t.click(pivotGrid.getFieldChooserButton()); + + await a11yCheck(t); +}).before(async () => createWidget('dxPivotGrid', createConfig())); + +test('header filter popup', async (t) => { + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const filterIcon = pivotGrid + .getColumnHeaderArea() + .getHeaderFilterIcon() + .element; + + await t.click(filterIcon); + + await a11yCheck(t); +}).before(async () => createWidget('dxPivotGrid', createConfig())); + +test('cell context menu', async (t) => { + const columnHeaderCell = Selector('.dx-pivotgrid-horizontal-headers td[tabindex]'); + + await t.rightClick(columnHeaderCell); + + await a11yCheck(t); +}).before(async () => createWidget('dxPivotGrid', createConfig())); diff --git a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts index faed3c6abb61..011f13651e09 100644 --- a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts +++ b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts @@ -112,6 +112,32 @@ const createConfig = () => ({ ? Selector(`.dx-pivotgridfieldchooser .dx-area-fields[group="${area}"]`) : Selector(`.dx-pivotgrid-fields-area[group="${area}"]`)); + test(`${testTitlePrefix}: Fields should be exposed as menu items of an area menubar`, async (t) => { + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + + if (isFieldChooser) { + await t.click(pivotGrid.getFieldChooserButton()); + } + + const rowAreaContainer = getAreaFieldsContainer('row'); + const menubar = rowAreaContainer.find('[role="menubar"]'); + const firstField = getField(pivotGrid, 'row', 0); + + await t + .expect(menubar.count) + .eql(1, 'the area has a single menubar') + .expect(menubar.getAttribute('aria-label')) + .eql('Row Fields', 'the menubar is labelled with the area name') + .expect(menubar.getAttribute('aria-description')) + .ok('the menubar describes the available keyboard interactions'); + + await t + .expect(firstField.getAttribute('role')) + .eql('menuitem', 'a field is exposed as a menu item') + .expect(firstField.getAttribute('aria-label')) + .eql('Field: Region, Sort order: ascending', 'the field label includes the name and the sorting state'); + }).before(async () => createWidget('dxPivotGrid', createConfig())); + ['filter', 'data', 'column', 'row'].forEach((area) => { test(`${testTitlePrefix}: Fields in ${area} area should form a single tab stop`, async (t) => { const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); @@ -381,3 +407,58 @@ test('FieldChooser: Should traverse fields in all areas by tab', async (t) => { .expect(dataFirstField.focused) .ok('first field in data area is focused'); }).before(async () => createWidget('dxPivotGrid', createConfig())); + +// TestCafe's pressKey does not support function keys (F1-F12), so the Shift+F10 +// shortcut is reproduced by dispatching the keydown event on the focused field. +const SHIFT_F10_KEYDOWN = { + key: 'F10', code: 'F10', shiftKey: true, bubbles: true, cancelable: true, +}; + +test('PivotGrid: Should open the context menu by Shift+F10 on a field', async (t) => { + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const firstField = pivotGrid.getRowHeaderArea().getField(0); + const contextMenuItem = Selector('.dx-context-menu .dx-menu-item-text').withText('Show Field Chooser'); + + await t + .click(firstField) + .expect(firstField.focused) + .ok('field is focused after click') + .dispatchEvent(firstField, 'keydown', SHIFT_F10_KEYDOWN) + .expect(contextMenuItem.visible) + .ok('the field context menu is shown after Shift+F10'); +}).before(async () => createWidget('dxPivotGrid', createConfig())); + +test('PivotGrid: Field should have focus after the context menu is closed', async (t) => { + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const firstField = pivotGrid.getRowHeaderArea().getField(0); + const contextMenuItem = Selector('.dx-context-menu .dx-menu-item-text').withText('Show Field Chooser'); + + await t + .click(firstField) + .dispatchEvent(firstField, 'keydown', SHIFT_F10_KEYDOWN) + .expect(contextMenuItem.visible) + .ok('the field context menu is shown after Shift+F10'); + + await t + .pressKey('esc') + .expect(firstField.focused) + .ok('the field is focused after the context menu is closed'); +}).before(async () => createWidget('dxPivotGrid', createConfig())); + +test('FieldChooser: Shift+F10 on a popup field should not open the grid context menu', async (t) => { + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + const fieldChooser = pivotGrid.getFieldChooser(); + const contextMenu = Selector('.dx-context-menu .dx-menu-item-text'); + + await t.click(pivotGrid.getFieldChooserButton()); + + const firstField = fieldChooser.getRowAreaItem(0); + + // Parity with the mouse: right-click on the popup fields does not show the + // grid context menu either. + await t + .click(firstField) + .dispatchEvent(firstField, 'keydown', SHIFT_F10_KEYDOWN) + .expect(contextMenu.exists) + .notOk('the grid context menu is not shown for a field chooser popup field'); +}).before(async () => createWidget('dxPivotGrid', createConfig())); diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.test.ts b/packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.test.ts new file mode 100644 index 000000000000..7f25d74ac399 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from '@jest/globals'; + +import { describeDataCellsWithHeaders } from './data_cell_description'; + +const createSection = (tag: 'thead' | 'tbody', rowsHtml: string): HTMLTableSectionElement => { + const table = document.createElement('table'); + table.innerHTML = `<${tag}>${rowsHtml}`; + return table.querySelector(tag) as HTMLTableSectionElement; +}; + +const getDescribingCells = (cell: HTMLTableCellElement | null): (Element | null)[] => (cell?.getAttribute('aria-describedby') ?? '') + .split(' ') + .filter((id) => !!id) + .map((id) => document.getElementById(id)); + +describe('describeDataCellsWithHeaders', () => { + it('should reference the column header path including colspan groups', () => { + const columns = createSection('thead', ` + 2015Grand Total + Q1Q2 + `); + const data = createSection('tbody', ` + 102030 + `); + document.body.append(columns, data); + + describeDataCellsWithHeaders(columns, undefined, data); + + const cells = data.querySelectorAll('td'); + expect(getDescribingCells(cells[0])).toEqual([ + document.getElementById('group'), + document.getElementById('q1'), + ]); + expect(getDescribingCells(cells[1])).toEqual([ + document.getElementById('group'), + document.getElementById('q2'), + ]); + expect(getDescribingCells(cells[2])).toEqual([ + document.getElementById('total'), + ]); + }); + + it('should reference the row header path including rowspan groups', () => { + const rows = createSection('tbody', ` + AfricaCairo + Luxor + `); + const data = createSection('tbody', ` + 10 + 20 + `); + document.body.append(rows, data); + + describeDataCellsWithHeaders(undefined, rows, data); + + const cells = data.querySelectorAll('td'); + expect(getDescribingCells(cells[0])).toEqual([ + document.getElementById('africa'), + document.getElementById('cairo'), + ]); + expect(getDescribingCells(cells[1])).toEqual([ + document.getElementById('africa'), + document.getElementById('luxor'), + ]); + }); + + it('should combine row and column paths in the row-then-column order', () => { + const columns = createSection('thead', '2015'); + const rows = createSection('tbody', 'Africa'); + const data = createSection('tbody', '10'); + document.body.append(columns, rows, data); + + describeDataCellsWithHeaders(columns, rows, data); + + expect(getDescribingCells(data.querySelector('td'))).toEqual([ + document.getElementById('r'), + document.getElementById('c'), + ]); + }); + + it('should respect the virtual scrolling index offset', () => { + const columns = createSection('thead', '20152016'); + const data = createSection('tbody', '1020'); + document.body.append(columns, data); + + describeDataCellsWithHeaders(columns, undefined, data); + + const cells = data.querySelectorAll('td'); + expect(getDescribingCells(cells[0])).toEqual([document.getElementById('c5')]); + expect(getDescribingCells(cells[1])).toEqual([document.getElementById('c6')]); + }); + + it('should reuse existing header cell ids and keep generated ones stable across runs', () => { + const columns = createSection('thead', '20152016'); + const data = createSection('tbody', '1020'); + document.body.append(columns, data); + + describeDataCellsWithHeaders(columns, undefined, data); + const generatedId = (columns.querySelectorAll('td')[1]).id; + describeDataCellsWithHeaders(columns, undefined, data); + + expect((columns.querySelectorAll('td')[0]).id).toBe('existing'); + expect((columns.querySelectorAll('td')[1]).id).toBe(generatedId); + expect(generatedId).toMatch(/^dx-/); + }); + + it('should remove a stale description when no header chains match', () => { + const data = createSection('tbody', '10'); + document.body.append(data); + + describeDataCellsWithHeaders(undefined, undefined, data); + + expect(data.querySelector('td')?.hasAttribute('aria-describedby')).toBe(false); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.ts b/packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.ts new file mode 100644 index 000000000000..fb27ee83b0a7 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.ts @@ -0,0 +1,101 @@ +import Guid from '@js/core/guid'; + +import { buildCellMatrix } from '../keyboard_navigation/table_cell_navigation'; + +type ChainMap = Map; + +const ensureId = (cell: HTMLTableCellElement): string => { + if (!cell.id) { + cell.id = `dx-${new Guid()}`; + } + return cell.id; +}; + +const getUniqueChainIds = (cells: (HTMLTableCellElement | undefined)[]): string => { + const chain: HTMLTableCellElement[] = []; + + cells.forEach((cell) => { + if (cell && !chain.includes(cell)) { + chain.push(cell); + } + }); + + return chain.map(ensureId).join(' '); +}; + +// The aria index attributes are affine to the rendered position (a constant +// virtual-scrolling offset per render), so a single sampled cell is enough to +// translate matrix positions into aria index keys. +const getColumnChains = (section: HTMLTableSectionElement | undefined): ChainMap => { + const chains: ChainMap = new Map(); + const matrix = section ? buildCellMatrix(section) : []; + const leafRow = matrix[matrix.length - 1] ?? []; + + const sampleCell = leafRow.find((cell) => cell?.hasAttribute('aria-colindex')); + if (!sampleCell) { + return chains; + } + const offset = Number(sampleCell.getAttribute('aria-colindex')) - leafRow.indexOf(sampleCell); + + leafRow.forEach((_, columnIndex) => { + chains.set( + String(columnIndex + offset), + getUniqueChainIds(matrix.map((row) => row[columnIndex])), + ); + }); + + return chains; +}; + +const getRowChains = (section: HTMLTableSectionElement | undefined): ChainMap => { + const chains: ChainMap = new Map(); + const matrix = section ? buildCellMatrix(section) : []; + + const sampleCell = matrix + .flat() + .find((cell) => cell?.hasAttribute('aria-rowindex')); + if (!sampleCell) { + return chains; + } + const sampleRow = (sampleCell.parentElement as HTMLTableRowElement).sectionRowIndex; + const offset = Number(sampleCell.getAttribute('aria-rowindex')) - sampleRow; + + matrix.forEach((row, rowIndex) => { + chains.set(String(rowIndex + offset), getUniqueChainIds(row)); + }); + + return chains; +}; + +// Associates every data cell with the row and column header cells covering it +// via aria-describedby. The header areas live in separate tables, so the +// native headers/id mechanism is not applicable; aria-describedby keeps the +// cell value as the accessible name and announces the header path as the +// description. +export function describeDataCellsWithHeaders( + columnsSection: HTMLTableSectionElement | undefined, + rowsSection: HTMLTableSectionElement | undefined, + dataSection: HTMLTableSectionElement | undefined, +): void { + if (!dataSection) { + return; + } + + const columnChains = getColumnChains(columnsSection); + const rowChains = getRowChains(rowsSection); + + Array.from(dataSection.rows).forEach((row) => { + Array.from(row.cells).forEach((cell) => { + const description = [ + rowChains.get(cell.getAttribute('aria-rowindex') ?? ''), + columnChains.get(cell.getAttribute('aria-colindex') ?? ''), + ].filter((ids) => !!ids).join(' '); + + if (description) { + cell.setAttribute('aria-describedby', description); + } else { + cell.removeAttribute('aria-describedby'); + } + }); + }); +} diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.test.ts b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.test.ts new file mode 100644 index 000000000000..cbd896a3d203 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.test.ts @@ -0,0 +1,53 @@ +import { + beforeAll, describe, expect, it, +} from '@jest/globals'; +import messageLocalization from '@js/common/core/localization/message'; + +import { getFieldItemA11yLabel, getFieldsAreaA11yDescription } from './a11y'; + +const AREA_DESCRIPTION = 'Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu'; + +beforeAll(() => { + // @ts-expect-error load is not declared on the localization typing + messageLocalization.load({ + en: { + 'dxPivotGrid-ariaFieldLabel': 'Field: {0}', + 'dxPivotGrid-ariaFieldHeaderFilterLabel': 'Header filter applied', + 'dxPivotGrid-ariaFieldSortAscLabel': 'Sort order: ascending', + 'dxPivotGrid-ariaFieldSortDescLabel': 'Sort order: descending', + 'dxPivotGrid-ariaFieldsAreaDescription': AREA_DESCRIPTION, + }, + }); +}); + +describe('getFieldItemA11yLabel', () => { + it('should contain only the field name by default', () => { + expect(getFieldItemA11yLabel('Region', {})).toBe('Field: Region'); + }); + + it('should append the ascending sorting state', () => { + expect(getFieldItemA11yLabel('Region', { sortOrder: 'asc' })) + .toBe('Field: Region, Sort order: ascending'); + }); + + it('should append the descending sorting state', () => { + expect(getFieldItemA11yLabel('Region', { sortOrder: 'desc' })) + .toBe('Field: Region, Sort order: descending'); + }); + + it('should append the header filter state', () => { + expect(getFieldItemA11yLabel('Region', { hasHeaderFilterValue: true })) + .toBe('Field: Region, Header filter applied'); + }); + + it('should compose all parts in a stable order', () => { + expect(getFieldItemA11yLabel('Region', { sortOrder: 'asc', hasHeaderFilterValue: true })) + .toBe('Field: Region, Header filter applied, Sort order: ascending'); + }); +}); + +describe('getFieldsAreaA11yDescription', () => { + it('should return the localized interaction description', () => { + expect(getFieldsAreaA11yDescription()).toBe(AREA_DESCRIPTION); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts new file mode 100644 index 000000000000..34e3424d1f76 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts @@ -0,0 +1,45 @@ +import messageLocalization from '@js/common/core/localization/message'; + +import type { SortOrderType } from './const'; +import { SORT_ORDER } from './const'; + +const I18N_KEYS = { + fieldLabel: 'dxPivotGrid-ariaFieldLabel', + hasHeaderFilter: 'dxPivotGrid-ariaFieldHeaderFilterLabel', + sortingAsc: 'dxPivotGrid-ariaFieldSortAscLabel', + sortingDesc: 'dxPivotGrid-ariaFieldSortDescLabel', + fieldsAreaDescription: 'dxPivotGrid-ariaFieldsAreaDescription', +}; + +const I18N_MESSAGE_SEPARATOR = ', '; + +export interface FieldItemA11yOptions { + sortOrder?: SortOrderType; + hasHeaderFilterValue?: boolean; +} + +const getSortingLabel = (sortOrder?: SortOrderType): string | null => { + switch (sortOrder) { + case SORT_ORDER.ascending: + return messageLocalization.format(I18N_KEYS.sortingAsc); + case SORT_ORDER.descending: + return messageLocalization.format(I18N_KEYS.sortingDesc); + default: + return null; + } +}; + +export const getFieldsAreaA11yDescription = (): string => messageLocalization + .format(I18N_KEYS.fieldsAreaDescription); + +export const getFieldItemA11yLabel = ( + caption: string, + { sortOrder, hasHeaderFilterValue }: FieldItemA11yOptions, +): string => [ + // @ts-expect-error format typing does not accept substitution args + messageLocalization.format(I18N_KEYS.fieldLabel, caption), + hasHeaderFilterValue ? messageLocalization.format(I18N_KEYS.hasHeaderFilter) : null, + getSortingLabel(sortOrder), +] + .filter((message) => !!message) + .join(I18N_MESSAGE_SEPARATOR); diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/m_field_chooser.ts b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/m_field_chooser.ts index b9278b15106f..01a4f1e31d27 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/m_field_chooser.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/m_field_chooser.ts @@ -12,6 +12,7 @@ import ContextMenu from '@js/ui/context_menu'; import TreeView from '@js/ui/tree_view'; import { foreachDataLevel, getCompareFunction } from '../m_widget_utils'; +import { getFieldsAreaA11yDescription } from './a11y'; import { ATTRIBUTES, CLASSES, ICONS } from './const'; import { FieldChooserBase } from './m_field_chooser_base'; @@ -548,6 +549,14 @@ export class FieldChooser extends FieldChooserBase { that.renderField(field, true).appendTo($container); } }); + + // A menubar without menu items is invalid ARIA, so an empty area stays + // without the role until fields are dropped into it. + const hasFields = !!$container.children().length; + $container + .attr('role', hasFields ? 'menubar' : null) + .attr('aria-label', hasFields ? that.option(`texts.${area}Fields`) : null) + .attr('aria-description', hasFields ? getFieldsAreaA11yDescription() : null); } _renderArea(container, area) { diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/m_field_chooser_base.ts b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/m_field_chooser_base.ts index d901fff7e551..38c947799b82 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/m_field_chooser_base.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/m_field_chooser_base.ts @@ -23,6 +23,8 @@ import type { RovingTabIndexComponent } from '../keyboard_navigation/roving_tab_ import { RovingTabIndex } from '../keyboard_navigation/roving_tab_index'; import { createPath, foreachTree } from '../m_widget_utils'; import SortableModule from '../sortable/m_sortable'; +import { getFieldItemA11yLabel } from './a11y'; +import type { SortOrderType } from './const'; import { ATTRIBUTES, CLASSES } from './const'; import { dragAndDropItemRender } from './dom'; import { reverseSortOrder, shouldCancelDragging } from './utils'; @@ -46,6 +48,11 @@ function isFieldNavigationEvent(e): boolean { && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey; } +function isContextMenuKeyEvent(e): boolean { + return e.type === 'keydown' + && (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')); +} + class HeaderFilterView extends HeaderFilterViewBase { _getSearchExpr(options, headerFilterOptions) { options.useDefaultSearchExpr = true; @@ -125,6 +132,7 @@ export class FieldChooserBase extends mixinWidget { allowFieldDragging: true, applyChangesMode: 'instantly', state: null, + onFieldContextMenuKeyDown: null, headerFilter: { width: 252, height: 325, @@ -185,6 +193,9 @@ export class FieldChooserBase extends mixinWidget { break; case 'applyChangesMode': case 'remoteSort': + case 'onFieldContextMenuKeyDown': + // The context menu handler is read from the options on each keydown, + // so a runtime change requires no re-render. break; case 'state': if (this._skipStateChange || !this._dataSource) { @@ -242,9 +253,25 @@ export class FieldChooserBase extends mixinWidget { $fieldElement.attr(ATTRIBUTES.itemGroup, field.groupName); } + $fieldElement + .attr('role', 'menuitem') + .attr('aria-label', this._getFieldItemAriaLabel(field, caption)); + return $fieldElement; } + private _getFieldItemAriaLabel(field, caption: string): string { + // The sort indicator is rendered for every sortable non-data field, with + // ascending shown by default, so the label mirrors the visible state. + const sortOrder = field.allowSorting && field.area !== 'data' + ? (field.sortOrder === 'desc' ? 'desc' : 'asc') as SortOrderType + : undefined; + const hasHeaderFilterValue = fieldHasHeaderFilter(this._dataSource, field) + && !!getMainGroupField(this._dataSource, field).filterValues?.length; + + return getFieldItemA11yLabel(caption, { sortOrder, hasHeaderFilterValue }); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars _clean(value?) { } @@ -406,6 +433,11 @@ export class FieldChooserBase extends mixinWidget { return; } + if (isContextMenuKeyEvent(e)) { + this._handleFieldContextMenuKey(e, field); + return; + } + if (!isClick) { return; } @@ -537,6 +569,24 @@ export class FieldChooserBase extends mixinWidget { return FIELD_NAVIGATION_DELTAS[key]; } + private _handleFieldContextMenuKey(e, field): void { + const onContextMenuKey = this.option('onFieldContextMenuKeyDown'); + + if (!field.area || !onContextMenuKey) { + return; + } + + const navigation = this._getFieldNavigation(field.area); + + // The event can bubble to another instance subscribed to an ancestor + // element; the field then does not belong to this instance's items. + if (!navigation.getItems().includes(e.currentTarget)) { + return; + } + + onContextMenuKey(e, navigation); + } + private _handleFieldNavigation(e, field): void { if (!field.area) { return; diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/fields_area/m_fields_area.ts b/packages/devextreme/js/__internal/grids/pivot_grid/fields_area/m_fields_area.ts index 4ae33e0c31d1..ef9448927f70 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/fields_area/m_fields_area.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/fields_area/m_fields_area.ts @@ -9,6 +9,7 @@ import Popup from '@js/ui/popup/ui.popup'; import { capitalize } from '@ts/core/utils/capitalize'; import { AreaItem } from '../area_item/m_area_item'; +import { getFieldsAreaA11yDescription } from '../field_chooser/a11y'; const DIV = '
'; @@ -46,7 +47,7 @@ class FieldsArea extends AreaItem { .attr('group', this._area); } - _createTableElement() { + _getAreaLabel() { const localizationMessageMap = { row: 'dxPivotGrid-rowFields', column: 'dxPivotGrid-columnFields', @@ -54,9 +55,12 @@ class FieldsArea extends AreaItem { filter: 'dxPivotGrid-filterFields', }; + return localizationMessage.format(localizationMessageMap[this._area]); + } + + _createTableElement() { return $('') - .attr('role', 'group') - .attr('aria-label', localizationMessage.format(localizationMessageMap[this._area])); + .attr('role', 'presentation'); } isVisible() { @@ -145,7 +149,10 @@ class FieldsArea extends AreaItem { const groupElement = this.groupElement(); const isVisible = this.isVisible(); const fieldChooserBase = that.component.$element().dxPivotGridFieldChooserBase('instance'); - const head = $('').addClass('dx-pivotgrid-fields-area-head').appendTo(tableElement); + const head = $('') + .addClass('dx-pivotgrid-fields-area-head') + .attr('role', 'presentation') + .appendTo(tableElement); const area = that._area; const row = $(''); @@ -153,12 +160,20 @@ class FieldsArea extends AreaItem { tableElement.addClass('dx-area-field-container'); if (!isVisible) { + // The reused table element may carry the menubar role from the previous + // render, so it is reset even though the hidden area skips rendering. + tableElement + .attr('role', 'presentation') + .removeAttr('aria-label') + .removeAttr('aria-description'); return; } each(data, (index: number, field) => { if (field.area === area && field.visible !== false) { - const td = $('
').append(fieldChooserBase.renderField(field, field.area === 'row')); + const td = $('') + .attr('role', 'none') + .append(fieldChooserBase.renderField(field, field.area === 'row')); const indicators = td.find('.dx-column-indicators'); if (indicators.length && that._shouldCreateButton()) { indicators.insertAfter((indicators as any).next()); @@ -167,7 +182,25 @@ class FieldsArea extends AreaItem { renderGroupConnector(field, data[index + 1], data[index - 1], td); } }); - if (!row.children().length) { + if (row.children().length) { + // Expose the fields as a menubar of menu items, consistent with the + // field chooser and CardView. The menubar role lives on the table + // element (it is invalid on a table row); thead/tr/td are kept out of + // the accessibility tree so the field items become the menubar's items. + row.attr('role', 'presentation'); + tableElement + .attr('role', 'menubar') + .attr('aria-label', this._getAreaLabel()) + .attr('aria-description', getFieldsAreaA11yDescription()); + } else { + // A menubar without menu items is invalid ARIA, so an empty area keeps + // the plain placeholder text and the table stays presentational. The + // role is reset explicitly because the table element is reused across + // re-renders. + tableElement + .attr('role', 'presentation') + .removeAttr('aria-label') + .removeAttr('aria-description'); $('').append($(DIV).addClass('dx-empty-area-text').text(this.option(`fieldPanel.texts.${area}FieldArea`))).appendTo(row); } diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts index f122d1881117..3e8a2c750766 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -26,6 +26,7 @@ import { current, isGeneric } from '@js/ui/themes'; import Widget from '@ts/core/widget/widget'; import gridCoreUtils from '@ts/grids/grid_core/m_utils'; +import { describeDataCellsWithHeaders } from './a11y/data_cell_description'; import { ChartIntegrationMixin } from './chart_integration/m_chart_integration'; import DataAreaImport from './data_area/m_data_area'; import DataControllerImport from './data_controller/m_data_controller'; @@ -1053,7 +1054,19 @@ class PivotGrid extends Widget { const cell = e.currentTarget; const navigation = this._getCellAreaNavigation(cell); - if (!navigation || !this._contextMenu) { + if (!navigation) { + return; + } + + this._showContextMenuFromKeyboard(e, navigation); + } + + _handleFieldContextMenuKeyDown(e, navigation: RovingTabIndex) { + this._showContextMenuFromKeyboard(e, navigation); + } + + _showContextMenuFromKeyboard(e, navigation: RovingTabIndex) { + if (!this._contextMenu) { return; } @@ -1449,6 +1462,7 @@ class PivotGrid extends Widget { visible: that.option('visible'), // @ts-expect-error ts-error remoteSort: that.option('scrolling.mode') === 'virtual', + onFieldContextMenuKeyDown: that._handleFieldContextMenuKeyDown.bind(that), }; if (that._fieldChooserBase) { @@ -1505,6 +1519,14 @@ class PivotGrid extends Widget { .attr('aria-owns', tableIds) .attr('aria-rowcount', this._dataController.totalRowCount()) .attr('aria-colcount', this._dataController.totalColumnCount()); + + // The areas can be partially stubbed in tests, so the sections are + // resolved defensively. + describeDataCellsWithHeaders( + columnsArea.headElement?.()?.get(0), + rowsArea.tableElement()?.children('tbody').get(0), + dataArea.tableElement()?.children('tbody').get(0), + ); } _update(isFirstDrawing) { diff --git a/packages/devextreme/js/localization/messages/ar.json b/packages/devextreme/js/localization/messages/ar.json index 0c0bbb248d7a..1494f4550c7b 100644 --- a/packages/devextreme/js/localization/messages/ar.json +++ b/packages/devextreme/js/localization/messages/ar.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "إفلات حقول البيانات هنا", "dxPivotGrid-rowFieldArea": "إفلات حقول الصف هنا", "dxPivotGrid-filterFieldArea": "إفلات حقول التصفية هنا", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "من {0} إلى {1}", "dxScheduler-ariaLabel": "المجدول. عرض {0}: {1} مع {2} مواعيد", diff --git a/packages/devextreme/js/localization/messages/bg.json b/packages/devextreme/js/localization/messages/bg.json index 2f366d172a26..5471b88035de 100644 --- a/packages/devextreme/js/localization/messages/bg.json +++ b/packages/devextreme/js/localization/messages/bg.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Пуснете полета с данни тук", "dxPivotGrid-rowFieldArea": "Пуснете полета на реда тук", "dxPivotGrid-filterFieldArea": "Пуснете полета за филтриране тук", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "от {0} до {1}", "dxScheduler-ariaLabel": "Планиращ. Изглед {0}: {1} с {2} срещи", diff --git a/packages/devextreme/js/localization/messages/ca.json b/packages/devextreme/js/localization/messages/ca.json index e6048fce76ea..b3ad530a7371 100644 --- a/packages/devextreme/js/localization/messages/ca.json +++ b/packages/devextreme/js/localization/messages/ca.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Deixar els camps de dades aquí", "dxPivotGrid-rowFieldArea": "Els camps de fila de la fila aquí", "dxPivotGrid-filterFieldArea": "Els camps de filtre de baixada aquí", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "de {0} a {1}", "dxScheduler-ariaLabel": "Planificador. Vista {0}: {1} amb {2} cites", diff --git a/packages/devextreme/js/localization/messages/cs.json b/packages/devextreme/js/localization/messages/cs.json index 8b0101b4a5c4..65d9167147de 100644 --- a/packages/devextreme/js/localization/messages/cs.json +++ b/packages/devextreme/js/localization/messages/cs.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Zde vložte pole dat", "dxPivotGrid-rowFieldArea": "Zde vložte pole řádků", "dxPivotGrid-filterFieldArea": "Zde vložte filtr pole", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "od {0} do {1}", "dxScheduler-ariaLabel": "Plánovač. Zobrazení {0}: {1} s {2} schůzkami", diff --git a/packages/devextreme/js/localization/messages/da.json b/packages/devextreme/js/localization/messages/da.json index d7a4c2742cca..beed4beca18b 100644 --- a/packages/devextreme/js/localization/messages/da.json +++ b/packages/devextreme/js/localization/messages/da.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Slip datafelter her", "dxPivotGrid-rowFieldArea": "Slip rækkefelter her", "dxPivotGrid-filterFieldArea": "Slip filterfelter her", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "fra {0} til {1}", "dxScheduler-ariaLabel": "Planlægger. {0} visning: {1} med {2} aftaler", diff --git a/packages/devextreme/js/localization/messages/de.json b/packages/devextreme/js/localization/messages/de.json index d14b0b30d688..000e35e50d45 100644 --- a/packages/devextreme/js/localization/messages/de.json +++ b/packages/devextreme/js/localization/messages/de.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Datenfelder hierher ziehen", "dxPivotGrid-rowFieldArea": "Zeilenfelder hierher ziehen", "dxPivotGrid-filterFieldArea": "Filterfelder hierher ziehen", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "von {0} bis {1}", "dxScheduler-ariaLabel": "Planer. {0} Ansicht: {1} mit {2} Terminen", diff --git a/packages/devextreme/js/localization/messages/el.json b/packages/devextreme/js/localization/messages/el.json index 2ace941c689c..2dea7899684c 100644 --- a/packages/devextreme/js/localization/messages/el.json +++ b/packages/devextreme/js/localization/messages/el.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Τοποθετήστε τα πεδία δεδομένων εδώ", "dxPivotGrid-rowFieldArea": "Τοποθετήστε τα πεδία γραμμών εδώ", "dxPivotGrid-filterFieldArea": "Τοποθετήστε τα πεδία φίλτρων εδώ", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "από {0} έως {1}", "dxScheduler-ariaLabel": "Χρονοδιάγραμμα. Προβολή {0}: {1} με {2} ραντεβού", diff --git a/packages/devextreme/js/localization/messages/en.json b/packages/devextreme/js/localization/messages/en.json index f7e629931ce9..cd72599595bf 100644 --- a/packages/devextreme/js/localization/messages/en.json +++ b/packages/devextreme/js/localization/messages/en.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Drop Data Fields Here", "dxPivotGrid-rowFieldArea": "Drop Row Fields Here", "dxPivotGrid-filterFieldArea": "Drop Filter Fields Here", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "from {0} to {1}", "dxScheduler-ariaLabel": "Scheduler. {0} view: {1} with {2} appointments", diff --git a/packages/devextreme/js/localization/messages/es.json b/packages/devextreme/js/localization/messages/es.json index 9dc3543fe8fc..8dfae2975cf1 100644 --- a/packages/devextreme/js/localization/messages/es.json +++ b/packages/devextreme/js/localization/messages/es.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Arrastra campos de dato aquí", "dxPivotGrid-rowFieldArea": "Arrastra campos de fila aquí", "dxPivotGrid-filterFieldArea": "Arrastra campos de filtro aquí", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "desde {0} hasta {1}", "dxScheduler-ariaLabel": "Programador. Vista {0}: {1} con {2} citas", diff --git a/packages/devextreme/js/localization/messages/fa.json b/packages/devextreme/js/localization/messages/fa.json index 3191cfcc8315..a1c5a186a7c7 100644 --- a/packages/devextreme/js/localization/messages/fa.json +++ b/packages/devextreme/js/localization/messages/fa.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "فیلدهای داده ای را به اینجا انتقال دهید (درگ نمایید)", "dxPivotGrid-rowFieldArea": "فیلدهای مربوط به سطر را به اینجا انتقال دهید (درگ نمایید)", "dxPivotGrid-filterFieldArea": "فیلدهای مربوط به فیلتر را به اینجا انتقال دهید (درگ نمایید)", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "از {0} تا {1}", "dxScheduler-ariaLabel": "زمانبندی. نمای {0}: {1} با {2} قرار ملاقات", diff --git a/packages/devextreme/js/localization/messages/fi.json b/packages/devextreme/js/localization/messages/fi.json index 8e90fb99eebf..65cc2ee5d63e 100644 --- a/packages/devextreme/js/localization/messages/fi.json +++ b/packages/devextreme/js/localization/messages/fi.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Pudota tietokentät tähän", "dxPivotGrid-rowFieldArea": "Pudota rivikentät tähän", "dxPivotGrid-filterFieldArea": "Pudota suodatinkentät tähän", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "{0}–{1}", "dxScheduler-ariaLabel": "Kalenteri. {0} näkymä: {1}, {2} tapaamista", diff --git a/packages/devextreme/js/localization/messages/fr.json b/packages/devextreme/js/localization/messages/fr.json index fd4c092b2250..6fd233c42e21 100644 --- a/packages/devextreme/js/localization/messages/fr.json +++ b/packages/devextreme/js/localization/messages/fr.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Déposer les champs de données ici", "dxPivotGrid-rowFieldArea": "Déposer les champs de ligne ici", "dxPivotGrid-filterFieldArea": "Déposer les champs de filtre ici", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "de {0} à {1}", "dxScheduler-ariaLabel": "Agenda. {0} vue : {1} avec {2} rendez-vous", diff --git a/packages/devextreme/js/localization/messages/hi.json b/packages/devextreme/js/localization/messages/hi.json index 1c1f79392548..8d5b3aff28fb 100644 --- a/packages/devextreme/js/localization/messages/hi.json +++ b/packages/devextreme/js/localization/messages/hi.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "डेटा फ़ील्ड यहां छोड़ें", "dxPivotGrid-rowFieldArea": "पंक्ति फ़ील्ड्स को यहां छोड़ें", "dxPivotGrid-filterFieldArea": "फ़िल्टर फ़ील्ड यहां छोड़ें", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "{0} से {1} तक", "dxScheduler-ariaLabel": "अनुसूचक. {0} दृश्य: {1} {2} नियुक्तियों के साथ", diff --git a/packages/devextreme/js/localization/messages/hu.json b/packages/devextreme/js/localization/messages/hu.json index f94c9113dc64..7b3965d86af5 100644 --- a/packages/devextreme/js/localization/messages/hu.json +++ b/packages/devextreme/js/localization/messages/hu.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Húzza az adatmezőket ide", "dxPivotGrid-rowFieldArea": "Húzza a sormezőket ide", "dxPivotGrid-filterFieldArea": "Húzza a szűrőmezőket ide", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "{0}-tól {1}-ig", "dxScheduler-ariaLabel": "Ütemező. {0} nézet: {1}, {2} eseménnyel", diff --git a/packages/devextreme/js/localization/messages/it.json b/packages/devextreme/js/localization/messages/it.json index 9318c530353e..044cffdbcdb8 100644 --- a/packages/devextreme/js/localization/messages/it.json +++ b/packages/devextreme/js/localization/messages/it.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Trascina qui i campi dati", "dxPivotGrid-rowFieldArea": "Trascina qui i campi riga", "dxPivotGrid-filterFieldArea": "Trascina qui i campi filtro", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "da {0} a {1}", "dxScheduler-ariaLabel": "Pianificatore. Vista {0}: {1} con {2} appuntamenti", diff --git a/packages/devextreme/js/localization/messages/ja.json b/packages/devextreme/js/localization/messages/ja.json index 80b25947b215..69dd7c165412 100644 --- a/packages/devextreme/js/localization/messages/ja.json +++ b/packages/devextreme/js/localization/messages/ja.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "データ フィールドをこちらへドラッグ&ドロップ", "dxPivotGrid-rowFieldArea": "行フィールドをこちらへドラッグ&ドロップ", "dxPivotGrid-filterFieldArea": "フィルター フィールドをこちらへドラッグ&ドロップ", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "{0} から {1} まで", "dxScheduler-ariaLabel": "スケジューラー。{0} ビュー: {2} 件の予定がある {1}", diff --git a/packages/devextreme/js/localization/messages/ko.json b/packages/devextreme/js/localization/messages/ko.json index cf9d06164045..b6c7d3c0ad1b 100644 --- a/packages/devextreme/js/localization/messages/ko.json +++ b/packages/devextreme/js/localization/messages/ko.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "여기에 데이터 필드를 놓으세요", "dxPivotGrid-rowFieldArea": "여기에 행 필드를 놓으세요", "dxPivotGrid-filterFieldArea": "여기에 필터 필드를 놓으세요", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "{0}부터 {1}까지", "dxScheduler-ariaLabel": "스케줄러. {0} 보기: {1}, 일정 {2}개", diff --git a/packages/devextreme/js/localization/messages/lt.json b/packages/devextreme/js/localization/messages/lt.json index 3c8e76cb9281..3e22b1d98c44 100644 --- a/packages/devextreme/js/localization/messages/lt.json +++ b/packages/devextreme/js/localization/messages/lt.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Vilkite duomenų laukus čia", "dxPivotGrid-rowFieldArea": "Vilkite eilučių laukus čia", "dxPivotGrid-filterFieldArea": "Vilkite filtrų laukus čia", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "nuo {0} iki {1}", "dxScheduler-ariaLabel": "Planuotuvas. {0} vaizdas: {1} su {2} įvykių", diff --git a/packages/devextreme/js/localization/messages/lv.json b/packages/devextreme/js/localization/messages/lv.json index 0d440c827f0d..ca52ca02489c 100644 --- a/packages/devextreme/js/localization/messages/lv.json +++ b/packages/devextreme/js/localization/messages/lv.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Velciet datu laukus šeit", "dxPivotGrid-rowFieldArea": "Velciet rindu laukus šeit", "dxPivotGrid-filterFieldArea": "Velciet filtra laukus šeit", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "no {0} līdz {1}", "dxScheduler-ariaLabel": "Plānotājs. {0} skats: {1} ar {2} apmeklējumiem", diff --git a/packages/devextreme/js/localization/messages/nb.json b/packages/devextreme/js/localization/messages/nb.json index 0c547e60b574..8059316faff5 100644 --- a/packages/devextreme/js/localization/messages/nb.json +++ b/packages/devextreme/js/localization/messages/nb.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Slipp datafelter her", "dxPivotGrid-rowFieldArea": "Slipp radfelter her", "dxPivotGrid-filterFieldArea": "Slipp filterfelter her", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "fra {0} til {1}", "dxScheduler-ariaLabel": "Planlegger. {0}-visning: {1} med {2} avtaler", diff --git a/packages/devextreme/js/localization/messages/nl.json b/packages/devextreme/js/localization/messages/nl.json index 4a7387378b25..19b577b2f8fc 100644 --- a/packages/devextreme/js/localization/messages/nl.json +++ b/packages/devextreme/js/localization/messages/nl.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Sleep gegevensvelden hier", "dxPivotGrid-rowFieldArea": "Sleep rijvelden hier", "dxPivotGrid-filterFieldArea": "Sleep filtervelden hier", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "van {0} tot {1}", "dxScheduler-ariaLabel": "Planner. {0} weergave: {1} met {2} afspraken", diff --git a/packages/devextreme/js/localization/messages/pl.json b/packages/devextreme/js/localization/messages/pl.json index 91610788ae6b..606db7fd3436 100644 --- a/packages/devextreme/js/localization/messages/pl.json +++ b/packages/devextreme/js/localization/messages/pl.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Przeciągnij pola danych tutaj", "dxPivotGrid-rowFieldArea": "Przeciągnij pola wiersza/-y tutaj", "dxPivotGrid-filterFieldArea": "Przeciągnij pola filtra tutaj", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "od {0} do {1}", "dxScheduler-ariaLabel": "Harmonogram. Widok {0}: {1}, liczba spotkań: {2}", diff --git a/packages/devextreme/js/localization/messages/pt-BR.json b/packages/devextreme/js/localization/messages/pt-BR.json index df9a1caec94b..187301ce3c46 100644 --- a/packages/devextreme/js/localization/messages/pt-BR.json +++ b/packages/devextreme/js/localization/messages/pt-BR.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Arraste os campos de dados até aqui", "dxPivotGrid-rowFieldArea": "Arraste os campos de linha até aqui", "dxPivotGrid-filterFieldArea": "Arraste os campos de filtro até aqui", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "de {0} a {1}", "dxScheduler-ariaLabel": "Agendador. Visualização {0}: {1} com {2} compromissos", diff --git a/packages/devextreme/js/localization/messages/pt-PT.json b/packages/devextreme/js/localization/messages/pt-PT.json index 8ede950af7c7..c65b214e866b 100644 --- a/packages/devextreme/js/localization/messages/pt-PT.json +++ b/packages/devextreme/js/localization/messages/pt-PT.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Arraste os campos de dados até aqui", "dxPivotGrid-rowFieldArea": "Arraste os campos de linha até aqui", "dxPivotGrid-filterFieldArea": "Arraste os campos de filtro até aqui", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "de {0} a {1}", "dxScheduler-ariaLabel": "Agendador. Visualização {0}: {1} com {2} compromissos", diff --git a/packages/devextreme/js/localization/messages/pt.json b/packages/devextreme/js/localization/messages/pt.json index 5637d420d6f3..c2d4c9acd648 100644 --- a/packages/devextreme/js/localization/messages/pt.json +++ b/packages/devextreme/js/localization/messages/pt.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Arraste os campos de dados até aqui", "dxPivotGrid-rowFieldArea": "Arraste os campos de linha até aqui", "dxPivotGrid-filterFieldArea": "Arraste os campos de filtro até aqui", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "de {0} a {1}", "dxScheduler-ariaLabel": "Agendador. Visualização {0}: {1} com {2} compromissos", diff --git a/packages/devextreme/js/localization/messages/ro.json b/packages/devextreme/js/localization/messages/ro.json index 2699cfbb2199..5952a344d34f 100644 --- a/packages/devextreme/js/localization/messages/ro.json +++ b/packages/devextreme/js/localization/messages/ro.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Plasați câmpurile de date aici", "dxPivotGrid-rowFieldArea": "Plasați câmpurile rândului aici", "dxPivotGrid-filterFieldArea": "Plasați câmpurile de filtrare aici", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "de la {0} până la {1}", "dxScheduler-ariaLabel": "Planificator. Vedere {0}: {1} cu {2} programări", diff --git a/packages/devextreme/js/localization/messages/ru.json b/packages/devextreme/js/localization/messages/ru.json index d8fc74d72b0a..cf99f090ca53 100644 --- a/packages/devextreme/js/localization/messages/ru.json +++ b/packages/devextreme/js/localization/messages/ru.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Перетащите поля данных cюда", "dxPivotGrid-rowFieldArea": "Перетащите поля строк cюда", "dxPivotGrid-filterFieldArea": "Перетащите поля фильтров cюда", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "с {0} по {1}", "dxScheduler-ariaLabel": "Планировщик. Вид {0}: {1}, записей: {2}", diff --git a/packages/devextreme/js/localization/messages/sl.json b/packages/devextreme/js/localization/messages/sl.json index 334073c40a76..642b37fe61a9 100644 --- a/packages/devextreme/js/localization/messages/sl.json +++ b/packages/devextreme/js/localization/messages/sl.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Povleci podatke tukaj", "dxPivotGrid-rowFieldArea": "Povleci vrstice tukaj", "dxPivotGrid-filterFieldArea": "Povleci filtre tukaj", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "od {0} do {1}", "dxScheduler-ariaLabel": "Načrtovalnik. Pogled {0}: {1} z {2} dogodki", diff --git a/packages/devextreme/js/localization/messages/sv.json b/packages/devextreme/js/localization/messages/sv.json index 2045b9878b15..bf5bcace26aa 100644 --- a/packages/devextreme/js/localization/messages/sv.json +++ b/packages/devextreme/js/localization/messages/sv.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Släpp datafält här", "dxPivotGrid-rowFieldArea": "Släpp radfält här", "dxPivotGrid-filterFieldArea": "Släpp filterfält här", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "från {0} till {1}", "dxScheduler-ariaLabel": "Schemaläggare. {0}-vy: {1} med {2} bokningar", diff --git a/packages/devextreme/js/localization/messages/tr.json b/packages/devextreme/js/localization/messages/tr.json index bcb5f6d3f6b2..be576db493a4 100644 --- a/packages/devextreme/js/localization/messages/tr.json +++ b/packages/devextreme/js/localization/messages/tr.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Veri Alanlarını Buraya Bırak", "dxPivotGrid-rowFieldArea": "Satır Alanlarını Buraya Bırak", "dxPivotGrid-filterFieldArea": "Filtre Alanlarını Buraya Bırak", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "{0} - {1}", "dxScheduler-ariaLabel": "Zamanlayıcı. {0} görünümü: {1}, {2} randevu", diff --git a/packages/devextreme/js/localization/messages/uk.json b/packages/devextreme/js/localization/messages/uk.json index dc1d48262688..27af6ea144aa 100644 --- a/packages/devextreme/js/localization/messages/uk.json +++ b/packages/devextreme/js/localization/messages/uk.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Перетягніть поля даних сюди", "dxPivotGrid-rowFieldArea": "Перетягніть поля рядків сюди", "dxPivotGrid-filterFieldArea": "Перетягніть поля фільтрації сюди", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "з {0} по {1}", "dxScheduler-ariaLabel": "Планувальник. Вид {0}: {1}, подій: {2}", diff --git a/packages/devextreme/js/localization/messages/vi.json b/packages/devextreme/js/localization/messages/vi.json index 82168e2b3296..aa8e4dac9760 100644 --- a/packages/devextreme/js/localization/messages/vi.json +++ b/packages/devextreme/js/localization/messages/vi.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "Thả các trường dữ liệu vào đây", "dxPivotGrid-rowFieldArea": "Thả các trường của hàng vào đây", "dxPivotGrid-filterFieldArea": "Thả bộ lọc các trường vào đây", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "từ {0} đến {1}", "dxScheduler-ariaLabel": "Lịch biểu. Chế độ xem {0}: {1} với {2} cuộc hẹn", diff --git a/packages/devextreme/js/localization/messages/zh-CN.json b/packages/devextreme/js/localization/messages/zh-CN.json index a60231563edf..9744677d9377 100644 --- a/packages/devextreme/js/localization/messages/zh-CN.json +++ b/packages/devextreme/js/localization/messages/zh-CN.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "将数据拖动到此处", "dxPivotGrid-rowFieldArea": "将行字段拖到此处", "dxPivotGrid-filterFieldArea": "拖动筛选字段到此处", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "从 {0} 到 {1}", "dxScheduler-ariaLabel": "日程表。{0} 视图:{1},共 {2} 个日程", diff --git a/packages/devextreme/js/localization/messages/zh-Hans.json b/packages/devextreme/js/localization/messages/zh-Hans.json index 6050b2769248..78df5e0e447f 100644 --- a/packages/devextreme/js/localization/messages/zh-Hans.json +++ b/packages/devextreme/js/localization/messages/zh-Hans.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "将数据拖动到此处", "dxPivotGrid-rowFieldArea": "将行字段拖到此处", "dxPivotGrid-filterFieldArea": "拖动筛选字段到此处", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "从 {0} 到 {1}", "dxScheduler-ariaLabel": "日程表。{0} 视图:{1},共 {2} 个日程", diff --git a/packages/devextreme/js/localization/messages/zh-tw.json b/packages/devextreme/js/localization/messages/zh-tw.json index 2ffba427171f..0fd718756c76 100644 --- a/packages/devextreme/js/localization/messages/zh-tw.json +++ b/packages/devextreme/js/localization/messages/zh-tw.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "將資料拖動到此處", "dxPivotGrid-rowFieldArea": "將列欄位拖曳至此", "dxPivotGrid-filterFieldArea": "拖動篩選欄位到此處", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "從 {0} 到 {1}", "dxScheduler-ariaLabel": "日程表。{0} 視圖:{1},共 {2} 個日程", diff --git a/packages/devextreme/js/localization/messages/zh.json b/packages/devextreme/js/localization/messages/zh.json index 6885efa6b29c..63bed7ec40b7 100644 --- a/packages/devextreme/js/localization/messages/zh.json +++ b/packages/devextreme/js/localization/messages/zh.json @@ -284,6 +284,11 @@ "dxPivotGrid-dataFieldArea": "将数据拖动到此处", "dxPivotGrid-rowFieldArea": "将行字段拖到此处", "dxPivotGrid-filterFieldArea": "拖动筛选字段到此处", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", + "dxPivotGrid-ariaFieldsAreaDescription": "Press Enter or Space to sort by the field, Alt + Down arrow to open the header filter, Shift + F10 to open the context menu", "dxScheduler-dateRange": "从 {0} 到 {1}", "dxScheduler-ariaLabel": "日程表。{0} 视图:{1},共 {2} 个日程", diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.markup.tests.js index a6123d08ad36..509f11554632 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.markup.tests.js @@ -239,7 +239,7 @@ QUnit.module('PivotGrid accessibility markup', { assert.equal($outerTable.attr('role'), 'presentation'); }); - QUnit.test('Field-area tables have role="group" and localized aria-label', function(assert) { + QUnit.test('Populated field-area tables are exposed as a labelled menubar', function(assert) { if(!windowUtils.hasWindow()) { assert.expect(0); return; @@ -261,8 +261,7 @@ QUnit.module('PivotGrid accessibility markup', { const expectedLabels = { row: 'Row Fields', column: 'Column Fields', - data: 'Data Fields', - filter: 'Filter Fields' + data: 'Data Fields' }; Object.keys(expectedLabels).forEach((area) => { @@ -270,9 +269,19 @@ QUnit.module('PivotGrid accessibility markup', { .find(`.dx-area-fields[group="${area}"] > table`); assert.strictEqual($table.length, 1, `${area} field table exists`); - assert.strictEqual($table.attr('role'), 'group'); - assert.strictEqual($table.attr('aria-label'), expectedLabels[area]); + assert.strictEqual($table.attr('role'), 'menubar', `${area} field table is a menubar`); + assert.strictEqual($table.attr('aria-label'), expectedLabels[area], `${area} menubar has localized label`); + assert.ok($table.attr('aria-description'), `${area} menubar has an interaction description`); }); + + // The filter area has no fields in this data source; a menubar without + // menu items is invalid ARIA, so its table stays presentational. + const $filterTable = pivotGrid.$element() + .find('.dx-area-fields[group="filter"] > table'); + + assert.strictEqual($filterTable.attr('role'), 'presentation', 'empty filter area table is presentational'); + assert.strictEqual($filterTable.attr('aria-label'), undefined, 'empty filter area table has no aria-label'); + assert.strictEqual($filterTable.attr('aria-description'), undefined, 'empty filter area table has no aria-description'); }); QUnit.test('Scrollable containers have no tabindex', function(assert) { @@ -330,6 +339,49 @@ QUnit.module('PivotGrid accessibility markup', { }); }); + QUnit.test('Data cells are described by their row and column header cells', function(assert) { + if(!windowUtils.hasWindow()) { + assert.expect(0); + return; + } + + const pivotGrid = createPivotGrid({ + width: 600, height: 400, + dataSource: createExpandableDataSource() + }); + this.clock.tick(10); + + const $columnArea = pivotGrid.$element().find('.dx-pivotgrid-horizontal-headers'); + const $rowsArea = pivotGrid.$element().find('.dx-pivotgrid-vertical-headers'); + const $dataArea = pivotGrid.$element().find('.dx-pivotgrid-area-data'); + + $dataArea.find('td').each((_, dataCell) => { + const describedBy = dataCell.getAttribute('aria-describedby'); + + assert.ok(describedBy, 'data cell has aria-describedby'); + + describedBy.split(' ').forEach((id) => { + const header = document.getElementById(id); + + assert.ok(header, `referenced element #${id} exists`); + assert.ok( + $columnArea.find(header).length || $rowsArea.find(header).length, + 'referenced element is a row or column header cell' + ); + }); + }); + + const firstDataCell = $dataArea.find('td').get(0); + const describedTexts = firstDataCell.getAttribute('aria-describedby') + .split(' ') + .map((id) => document.getElementById(id).textContent.trim()); + const firstRowHeaderText = $rowsArea.find('td').first().text().trim(); + const firstColumnHeaderText = $columnArea.find('td').first().text().trim(); + + assert.ok(describedTexts.includes(firstRowHeaderText), 'description includes the row header caption'); + assert.ok(describedTexts.includes(firstColumnHeaderText), 'description includes the column header caption'); + }); + QUnit.test('Row header and data cells have aria-rowindex along shared row axis', function(assert) { if(!windowUtils.hasWindow()) { assert.expect(0);