From 30254117b281acdbcbb51bd5118895beba282c42 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 16 Jul 2026 08:55:39 -0300 Subject: [PATCH 01/12] PivotGrid - KBN - Add ARIA attributes to area fields --- .../tests/common/pivotGrid/kbn/fields.ts | 29 ++++++++++++ .../pivot_grid/field_chooser/a11y.test.ts | 44 +++++++++++++++++++ .../grids/pivot_grid/field_chooser/a11y.ts | 41 +++++++++++++++++ .../field_chooser/m_field_chooser.ts | 7 +++ .../field_chooser/m_field_chooser_base.ts | 18 ++++++++ .../pivot_grid/fields_area/m_fields_area.ts | 36 ++++++++++++--- .../js/localization/messages/ar.json | 4 ++ .../js/localization/messages/bg.json | 4 ++ .../js/localization/messages/ca.json | 4 ++ .../js/localization/messages/cs.json | 4 ++ .../js/localization/messages/da.json | 4 ++ .../js/localization/messages/de.json | 4 ++ .../js/localization/messages/el.json | 4 ++ .../js/localization/messages/en.json | 4 ++ .../js/localization/messages/es.json | 4 ++ .../js/localization/messages/fa.json | 4 ++ .../js/localization/messages/fi.json | 4 ++ .../js/localization/messages/fr.json | 4 ++ .../js/localization/messages/hi.json | 4 ++ .../js/localization/messages/hu.json | 4 ++ .../js/localization/messages/it.json | 4 ++ .../js/localization/messages/ja.json | 4 ++ .../js/localization/messages/ko.json | 4 ++ .../js/localization/messages/lt.json | 4 ++ .../js/localization/messages/lv.json | 4 ++ .../js/localization/messages/nb.json | 4 ++ .../js/localization/messages/nl.json | 4 ++ .../js/localization/messages/pl.json | 4 ++ .../js/localization/messages/pt-BR.json | 4 ++ .../js/localization/messages/pt-PT.json | 4 ++ .../js/localization/messages/pt.json | 4 ++ .../js/localization/messages/ro.json | 4 ++ .../js/localization/messages/ru.json | 4 ++ .../js/localization/messages/sl.json | 4 ++ .../js/localization/messages/sv.json | 4 ++ .../js/localization/messages/tr.json | 4 ++ .../js/localization/messages/uk.json | 4 ++ .../js/localization/messages/vi.json | 4 ++ .../js/localization/messages/zh-CN.json | 4 ++ .../js/localization/messages/zh-Hans.json | 4 ++ .../js/localization/messages/zh-tw.json | 4 ++ .../js/localization/messages/zh.json | 4 ++ .../pivotGrid.markup.tests.js | 17 ++++--- 43 files changed, 325 insertions(+), 11 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.test.ts create mode 100644 packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts diff --git a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts index 86a80a6d87a2..2d8973d973ef 100644 --- a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts +++ b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts @@ -1,5 +1,6 @@ import PivotGrid from 'devextreme-testcafe-models/pivotGrid'; import HeaderFilter from 'devextreme-testcafe-models/dataGrid/headers/headerFilter'; +import { Selector } from 'testcafe'; import url from '../../../../helpers/getPageUrl'; import { createWidget } from '../../../../helpers/createWidget'; @@ -107,6 +108,34 @@ const createConfig = () => ({ const testTitlePrefix = isFieldChooser ? 'Field Chooser' : 'PivotGrid'; + const getAreaFieldsContainer = (area: string) => (isFieldChooser + ? 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'); + + 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 be focusable by tab`, async (t) => { const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); 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..9f4e74a69d1f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.test.ts @@ -0,0 +1,44 @@ +import { + beforeAll, describe, expect, it, +} from '@jest/globals'; +import messageLocalization from '@js/common/core/localization/message'; + +import { getFieldItemA11yLabel } from './a11y'; + +describe('getFieldItemA11yLabel', () => { + 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-ariaFieldISortAscLabel': 'Sort order: ascending', + 'dxPivotGrid-ariaFieldSortDescLabel': 'Sort order: descending', + }, + }); + }); + + 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'); + }); +}); 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..69f94216181e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts @@ -0,0 +1,41 @@ +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-ariaFieldISortAscLabel', + sortingDesc: 'dxPivotGrid-ariaFieldSortDescLabel', +}; + +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 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 bf0bb3312ec2..8c5a845897e9 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 @@ -547,6 +547,13 @@ 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); } _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 3323acb54ba1..7ac5bc9477b1 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 @@ -21,6 +21,8 @@ import sortingMixin from '@ts/grids/grid_core/sorting/m_sorting_mixin'; 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'; @@ -211,9 +213,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?) { } 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..7ca2d455c2e9 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 @@ -46,7 +46,7 @@ class FieldsArea extends AreaItem { .attr('group', this._area); } - _createTableElement() { + _getAreaLabel() { const localizationMessageMap = { row: 'dxPivotGrid-rowFields', column: 'dxPivotGrid-columnFields', @@ -54,9 +54,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 +148,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 = $(''); @@ -158,7 +164,9 @@ class FieldsArea extends AreaItem { each(data, (index: number, field) => { if (field.area === area && field.visible !== false) { - const td = $(' + + `); + const data = createSection('tbody', ` + + `); + 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', ` + + + `); + const data = createSection('tbody', ` + + + `); + 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', ''); + const rows = createSection('tbody', ''); + const data = createSection('tbody', ''); + 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', ''); + const data = createSection('tbody', ''); + 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', ''); + const data = createSection('tbody', ''); + 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', ''); + 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/m_widget.ts b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts index d664a42739c0..b56d91979261 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'; @@ -1518,6 +1519,12 @@ class PivotGrid extends Widget { .attr('aria-owns', tableIds) .attr('aria-rowcount', this._dataController.totalRowCount()) .attr('aria-colcount', this._dataController.totalColumnCount()); + + describeDataCellsWithHeaders( + columnsArea.headElement().get(0), + rowsArea.tableElement().children('tbody').get(0), + dataArea.tableElement().children('tbody').get(0), + ); } _update(isFirstDrawing) { 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 2018b9dbc554..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 @@ -339,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); From c201add42b6a2fda6bb0ad62abe9512de18b220b Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 17 Jul 2026 19:21:51 -0300 Subject: [PATCH 09/12] PivotGrid - Resolve area sections defensively for partially stubbed areas --- .../devextreme/js/__internal/grids/pivot_grid/m_widget.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 b56d91979261..3e8a2c750766 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -1520,10 +1520,12 @@ class PivotGrid extends Widget { .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), + columnsArea.headElement?.()?.get(0), + rowsArea.tableElement()?.children('tbody').get(0), + dataArea.tableElement()?.children('tbody').get(0), ); } From e32ae6d3f9b73620987e804bee8f883ce54346b6 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 23 Jul 2026 10:25:00 -0300 Subject: [PATCH 10/12] PivotGrid - Revert data-cell aria-describedby; deliver field-area keyboard hints via aria-label --- .../tests/common/pivotGrid/kbn/fields.ts | 6 +- .../a11y/data_cell_description.test.ts | 115 ------------------ .../pivot_grid/a11y/data_cell_description.ts | 101 --------------- .../pivot_grid/field_chooser/a11y.test.ts | 8 +- .../grids/pivot_grid/field_chooser/a11y.ts | 6 + .../field_chooser/m_field_chooser.ts | 5 +- .../pivot_grid/fields_area/m_fields_area.ts | 11 +- .../__internal/grids/pivot_grid/m_widget.ts | 9 -- .../pivotGrid.markup.tests.js | 49 +------- 9 files changed, 25 insertions(+), 285 deletions(-) delete mode 100644 packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.test.ts delete mode 100644 packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.ts diff --git a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts index 011f13651e09..6f405daa35f3 100644 --- a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts +++ b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts @@ -127,9 +127,9 @@ const createConfig = () => ({ .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'); + .contains('Row Fields', 'the menubar label starts with the area name') + .expect(menubar.getAttribute('aria-label')) + .contains('Shift + F10', 'the menubar label also describes the keyboard interactions'); await t .expect(firstField.getAttribute('role')) 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 deleted file mode 100644 index 7f25d74ac399..000000000000 --- a/packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -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', ` - - - `); - const data = createSection('tbody', ` - - `); - 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', ` - - - `); - const data = createSection('tbody', ` - - - `); - 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', ''); - const rows = createSection('tbody', ''); - const data = createSection('tbody', ''); - 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', ''); - const data = createSection('tbody', ''); - 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', ''); - const data = createSection('tbody', ''); - 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', ''); - 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 deleted file mode 100644 index fb27ee83b0a7..000000000000 --- a/packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.ts +++ /dev/null @@ -1,101 +0,0 @@ -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 index cbd896a3d203..864005ff611d 100644 --- 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 @@ -3,7 +3,7 @@ import { } from '@jest/globals'; import messageLocalization from '@js/common/core/localization/message'; -import { getFieldItemA11yLabel, getFieldsAreaA11yDescription } from './a11y'; +import { getFieldItemA11yLabel, getFieldsAreaA11yDescription, getFieldsAreaA11yLabel } 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'; @@ -51,3 +51,9 @@ describe('getFieldsAreaA11yDescription', () => { expect(getFieldsAreaA11yDescription()).toBe(AREA_DESCRIPTION); }); }); + +describe('getFieldsAreaA11yLabel', () => { + it('should compose the area name with the interaction description', () => { + expect(getFieldsAreaA11yLabel('Row Fields')).toBe(`Row Fields. ${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 index 34e3424d1f76..419d85b2fea1 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts @@ -32,6 +32,12 @@ const getSortingLabel = (sortOrder?: SortOrderType): string | null => { export const getFieldsAreaA11yDescription = (): string => messageLocalization .format(I18N_KEYS.fieldsAreaDescription); +// The keyboard instructions are folded into the area's aria-label rather than +// exposed via aria-description: aria-description is an ARIA 1.3 draft attribute +// that screen readers do not read reliably, whereas the label composition +// mirrors how DataGrid/TreeList surface such instructions. +export const getFieldsAreaA11yLabel = (areaLabel: string): string => `${areaLabel}. ${getFieldsAreaA11yDescription()}`; + export const getFieldItemA11yLabel = ( caption: string, { sortOrder, hasHeaderFilterValue }: FieldItemA11yOptions, 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 01a4f1e31d27..c64e21649bae 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,7 +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 { getFieldsAreaA11yLabel } from './a11y'; import { ATTRIBUTES, CLASSES, ICONS } from './const'; import { FieldChooserBase } from './m_field_chooser_base'; @@ -555,8 +555,7 @@ export class FieldChooser extends FieldChooserBase { 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); + .attr('aria-label', hasFields ? getFieldsAreaA11yLabel(that.option(`texts.${area}Fields`)) : null); } _renderArea(container, area) { 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 ef9448927f70..fc5e16ed8ade 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,7 +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'; +import { getFieldsAreaA11yLabel } from '../field_chooser/a11y'; const DIV = '
'; @@ -164,8 +164,7 @@ class FieldsArea extends AreaItem { // render, so it is reset even though the hidden area skips rendering. tableElement .attr('role', 'presentation') - .removeAttr('aria-label') - .removeAttr('aria-description'); + .removeAttr('aria-label'); return; } @@ -190,8 +189,7 @@ class FieldsArea extends AreaItem { row.attr('role', 'presentation'); tableElement .attr('role', 'menubar') - .attr('aria-label', this._getAreaLabel()) - .attr('aria-description', getFieldsAreaA11yDescription()); + .attr('aria-label', getFieldsAreaA11yLabel(this._getAreaLabel())); } else { // A menubar without menu items is invalid ARIA, so an empty area keeps // the plain placeholder text and the table stays presentational. The @@ -199,8 +197,7 @@ class FieldsArea extends AreaItem { // re-renders. tableElement .attr('role', 'presentation') - .removeAttr('aria-label') - .removeAttr('aria-description'); + .removeAttr('aria-label'); $('
').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 +175,23 @@ 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()); + } 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'); $('').append($(DIV).addClass('dx-empty-area-text').text(this.option(`fieldPanel.texts.${area}FieldArea`))).appendTo(row); } diff --git a/packages/devextreme/js/localization/messages/ar.json b/packages/devextreme/js/localization/messages/ar.json index 0c0bbb248d7a..f1416dcd61f2 100644 --- a/packages/devextreme/js/localization/messages/ar.json +++ b/packages/devextreme/js/localization/messages/ar.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "إفلات حقول البيانات هنا", "dxPivotGrid-rowFieldArea": "إفلات حقول الصف هنا", "dxPivotGrid-filterFieldArea": "إفلات حقول التصفية هنا", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..8aed0475e472 100644 --- a/packages/devextreme/js/localization/messages/bg.json +++ b/packages/devextreme/js/localization/messages/bg.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "Пуснете полета с данни тук", "dxPivotGrid-rowFieldArea": "Пуснете полета на реда тук", "dxPivotGrid-filterFieldArea": "Пуснете полета за филтриране тук", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..c8d8929854f9 100644 --- a/packages/devextreme/js/localization/messages/ca.json +++ b/packages/devextreme/js/localization/messages/ca.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..90d1b6035aff 100644 --- a/packages/devextreme/js/localization/messages/cs.json +++ b/packages/devextreme/js/localization/messages/cs.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..dcaf4c5437e3 100644 --- a/packages/devextreme/js/localization/messages/da.json +++ b/packages/devextreme/js/localization/messages/da.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..9d34fedf0b77 100644 --- a/packages/devextreme/js/localization/messages/de.json +++ b/packages/devextreme/js/localization/messages/de.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..2a0291976ece 100644 --- a/packages/devextreme/js/localization/messages/el.json +++ b/packages/devextreme/js/localization/messages/el.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "Τοποθετήστε τα πεδία δεδομένων εδώ", "dxPivotGrid-rowFieldArea": "Τοποθετήστε τα πεδία γραμμών εδώ", "dxPivotGrid-filterFieldArea": "Τοποθετήστε τα πεδία φίλτρων εδώ", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..600c85c6f71a 100644 --- a/packages/devextreme/js/localization/messages/en.json +++ b/packages/devextreme/js/localization/messages/en.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..96636b8ad054 100644 --- a/packages/devextreme/js/localization/messages/es.json +++ b/packages/devextreme/js/localization/messages/es.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..8c69074ae12b 100644 --- a/packages/devextreme/js/localization/messages/fa.json +++ b/packages/devextreme/js/localization/messages/fa.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "فیلدهای داده ای را به اینجا انتقال دهید (درگ نمایید)", "dxPivotGrid-rowFieldArea": "فیلدهای مربوط به سطر را به اینجا انتقال دهید (درگ نمایید)", "dxPivotGrid-filterFieldArea": "فیلدهای مربوط به فیلتر را به اینجا انتقال دهید (درگ نمایید)", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..859c86c87f35 100644 --- a/packages/devextreme/js/localization/messages/fi.json +++ b/packages/devextreme/js/localization/messages/fi.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..be2247c2687c 100644 --- a/packages/devextreme/js/localization/messages/fr.json +++ b/packages/devextreme/js/localization/messages/fr.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..cf6378cce52e 100644 --- a/packages/devextreme/js/localization/messages/hi.json +++ b/packages/devextreme/js/localization/messages/hi.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "डेटा फ़ील्ड यहां छोड़ें", "dxPivotGrid-rowFieldArea": "पंक्ति फ़ील्ड्स को यहां छोड़ें", "dxPivotGrid-filterFieldArea": "फ़िल्टर फ़ील्ड यहां छोड़ें", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..5e8a0690148b 100644 --- a/packages/devextreme/js/localization/messages/hu.json +++ b/packages/devextreme/js/localization/messages/hu.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..921d88720022 100644 --- a/packages/devextreme/js/localization/messages/it.json +++ b/packages/devextreme/js/localization/messages/it.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..086e3a745449 100644 --- a/packages/devextreme/js/localization/messages/ja.json +++ b/packages/devextreme/js/localization/messages/ja.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "データ フィールドをこちらへドラッグ&ドロップ", "dxPivotGrid-rowFieldArea": "行フィールドをこちらへドラッグ&ドロップ", "dxPivotGrid-filterFieldArea": "フィルター フィールドをこちらへドラッグ&ドロップ", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..c4ade42c2f02 100644 --- a/packages/devextreme/js/localization/messages/ko.json +++ b/packages/devextreme/js/localization/messages/ko.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "여기에 데이터 필드를 놓으세요", "dxPivotGrid-rowFieldArea": "여기에 행 필드를 놓으세요", "dxPivotGrid-filterFieldArea": "여기에 필터 필드를 놓으세요", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..1cabd91244c2 100644 --- a/packages/devextreme/js/localization/messages/lt.json +++ b/packages/devextreme/js/localization/messages/lt.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..a12bab972580 100644 --- a/packages/devextreme/js/localization/messages/lv.json +++ b/packages/devextreme/js/localization/messages/lv.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..a9db3cd1f13d 100644 --- a/packages/devextreme/js/localization/messages/nb.json +++ b/packages/devextreme/js/localization/messages/nb.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..096d4d9b6011 100644 --- a/packages/devextreme/js/localization/messages/nl.json +++ b/packages/devextreme/js/localization/messages/nl.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..b8ed1ffae65e 100644 --- a/packages/devextreme/js/localization/messages/pl.json +++ b/packages/devextreme/js/localization/messages/pl.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..496a6aa856ef 100644 --- a/packages/devextreme/js/localization/messages/pt-BR.json +++ b/packages/devextreme/js/localization/messages/pt-BR.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..7a51c9f2ac53 100644 --- a/packages/devextreme/js/localization/messages/pt-PT.json +++ b/packages/devextreme/js/localization/messages/pt-PT.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..940a30b7502b 100644 --- a/packages/devextreme/js/localization/messages/pt.json +++ b/packages/devextreme/js/localization/messages/pt.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..c973681f639f 100644 --- a/packages/devextreme/js/localization/messages/ro.json +++ b/packages/devextreme/js/localization/messages/ro.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..27b00e1adf60 100644 --- a/packages/devextreme/js/localization/messages/ru.json +++ b/packages/devextreme/js/localization/messages/ru.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "Перетащите поля данных cюда", "dxPivotGrid-rowFieldArea": "Перетащите поля строк cюда", "dxPivotGrid-filterFieldArea": "Перетащите поля фильтров cюда", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..f49b686be821 100644 --- a/packages/devextreme/js/localization/messages/sl.json +++ b/packages/devextreme/js/localization/messages/sl.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..267b4ff95f4d 100644 --- a/packages/devextreme/js/localization/messages/sv.json +++ b/packages/devextreme/js/localization/messages/sv.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..c7469e5b211d 100644 --- a/packages/devextreme/js/localization/messages/tr.json +++ b/packages/devextreme/js/localization/messages/tr.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..95f8529862bd 100644 --- a/packages/devextreme/js/localization/messages/uk.json +++ b/packages/devextreme/js/localization/messages/uk.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "Перетягніть поля даних сюди", "dxPivotGrid-rowFieldArea": "Перетягніть поля рядків сюди", "dxPivotGrid-filterFieldArea": "Перетягніть поля фільтрації сюди", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..59e701cfde0b 100644 --- a/packages/devextreme/js/localization/messages/vi.json +++ b/packages/devextreme/js/localization/messages/vi.json @@ -284,6 +284,10 @@ "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-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..6c57f39d4314 100644 --- a/packages/devextreme/js/localization/messages/zh-CN.json +++ b/packages/devextreme/js/localization/messages/zh-CN.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "将数据拖动到此处", "dxPivotGrid-rowFieldArea": "将行字段拖到此处", "dxPivotGrid-filterFieldArea": "拖动筛选字段到此处", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..b760713eef61 100644 --- a/packages/devextreme/js/localization/messages/zh-Hans.json +++ b/packages/devextreme/js/localization/messages/zh-Hans.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "将数据拖动到此处", "dxPivotGrid-rowFieldArea": "将行字段拖到此处", "dxPivotGrid-filterFieldArea": "拖动筛选字段到此处", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..8cffe465b044 100644 --- a/packages/devextreme/js/localization/messages/zh-tw.json +++ b/packages/devextreme/js/localization/messages/zh-tw.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "將資料拖動到此處", "dxPivotGrid-rowFieldArea": "將列欄位拖曳至此", "dxPivotGrid-filterFieldArea": "拖動篩選欄位到此處", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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..1af2b7ffa1bb 100644 --- a/packages/devextreme/js/localization/messages/zh.json +++ b/packages/devextreme/js/localization/messages/zh.json @@ -284,6 +284,10 @@ "dxPivotGrid-dataFieldArea": "将数据拖动到此处", "dxPivotGrid-rowFieldArea": "将行字段拖到此处", "dxPivotGrid-filterFieldArea": "拖动筛选字段到此处", + "dxPivotGrid-ariaFieldLabel": "Field: {0}", + "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", + "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "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 1438b0258bfc..6d5d8d0199bb 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,17 @@ 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`); }); + + // 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'); }); QUnit.test('Scrollable containers have no tabindex', function(assert) { From eaa408315b5966cc684481dceda5c0b0047df06b Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 16 Jul 2026 13:14:39 -0300 Subject: [PATCH 02/12] PivotGrid - KBN - Fix ascending sort ARIA field message key typo --- .../js/__internal/grids/pivot_grid/field_chooser/a11y.test.ts | 2 +- .../js/__internal/grids/pivot_grid/field_chooser/a11y.ts | 2 +- packages/devextreme/js/localization/messages/ar.json | 2 +- packages/devextreme/js/localization/messages/bg.json | 2 +- packages/devextreme/js/localization/messages/ca.json | 2 +- packages/devextreme/js/localization/messages/cs.json | 2 +- packages/devextreme/js/localization/messages/da.json | 2 +- packages/devextreme/js/localization/messages/de.json | 2 +- packages/devextreme/js/localization/messages/el.json | 2 +- packages/devextreme/js/localization/messages/en.json | 2 +- packages/devextreme/js/localization/messages/es.json | 2 +- packages/devextreme/js/localization/messages/fa.json | 2 +- packages/devextreme/js/localization/messages/fi.json | 2 +- packages/devextreme/js/localization/messages/fr.json | 2 +- packages/devextreme/js/localization/messages/hi.json | 2 +- packages/devextreme/js/localization/messages/hu.json | 2 +- packages/devextreme/js/localization/messages/it.json | 2 +- packages/devextreme/js/localization/messages/ja.json | 2 +- packages/devextreme/js/localization/messages/ko.json | 2 +- packages/devextreme/js/localization/messages/lt.json | 2 +- packages/devextreme/js/localization/messages/lv.json | 2 +- packages/devextreme/js/localization/messages/nb.json | 2 +- packages/devextreme/js/localization/messages/nl.json | 2 +- packages/devextreme/js/localization/messages/pl.json | 2 +- packages/devextreme/js/localization/messages/pt-BR.json | 2 +- packages/devextreme/js/localization/messages/pt-PT.json | 2 +- packages/devextreme/js/localization/messages/pt.json | 2 +- packages/devextreme/js/localization/messages/ro.json | 2 +- packages/devextreme/js/localization/messages/ru.json | 2 +- packages/devextreme/js/localization/messages/sl.json | 2 +- packages/devextreme/js/localization/messages/sv.json | 2 +- packages/devextreme/js/localization/messages/tr.json | 2 +- packages/devextreme/js/localization/messages/uk.json | 2 +- packages/devextreme/js/localization/messages/vi.json | 2 +- packages/devextreme/js/localization/messages/zh-CN.json | 2 +- packages/devextreme/js/localization/messages/zh-Hans.json | 2 +- packages/devextreme/js/localization/messages/zh-tw.json | 2 +- packages/devextreme/js/localization/messages/zh.json | 2 +- 38 files changed, 38 insertions(+), 38 deletions(-) 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 index 9f4e74a69d1f..2aa7ec1782c8 100644 --- 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 @@ -12,7 +12,7 @@ describe('getFieldItemA11yLabel', () => { en: { 'dxPivotGrid-ariaFieldLabel': 'Field: {0}', 'dxPivotGrid-ariaFieldHeaderFilterLabel': 'Header filter applied', - 'dxPivotGrid-ariaFieldISortAscLabel': 'Sort order: ascending', + 'dxPivotGrid-ariaFieldSortAscLabel': 'Sort order: ascending', 'dxPivotGrid-ariaFieldSortDescLabel': 'Sort order: descending', }, }); 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 index 69f94216181e..ba96debfe86b 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts @@ -6,7 +6,7 @@ import { SORT_ORDER } from './const'; const I18N_KEYS = { fieldLabel: 'dxPivotGrid-ariaFieldLabel', hasHeaderFilter: 'dxPivotGrid-ariaFieldHeaderFilterLabel', - sortingAsc: 'dxPivotGrid-ariaFieldISortAscLabel', + sortingAsc: 'dxPivotGrid-ariaFieldSortAscLabel', sortingDesc: 'dxPivotGrid-ariaFieldSortDescLabel', }; diff --git a/packages/devextreme/js/localization/messages/ar.json b/packages/devextreme/js/localization/messages/ar.json index f1416dcd61f2..a4fc5ae8d3cc 100644 --- a/packages/devextreme/js/localization/messages/ar.json +++ b/packages/devextreme/js/localization/messages/ar.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "إفلات حقول التصفية هنا", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "من {0} إلى {1}", diff --git a/packages/devextreme/js/localization/messages/bg.json b/packages/devextreme/js/localization/messages/bg.json index 8aed0475e472..5e2996351015 100644 --- a/packages/devextreme/js/localization/messages/bg.json +++ b/packages/devextreme/js/localization/messages/bg.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Пуснете полета за филтриране тук", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "от {0} до {1}", diff --git a/packages/devextreme/js/localization/messages/ca.json b/packages/devextreme/js/localization/messages/ca.json index c8d8929854f9..b51a0f2af8e9 100644 --- a/packages/devextreme/js/localization/messages/ca.json +++ b/packages/devextreme/js/localization/messages/ca.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Els camps de filtre de baixada aquí", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "de {0} a {1}", diff --git a/packages/devextreme/js/localization/messages/cs.json b/packages/devextreme/js/localization/messages/cs.json index 90d1b6035aff..799b44364db5 100644 --- a/packages/devextreme/js/localization/messages/cs.json +++ b/packages/devextreme/js/localization/messages/cs.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Zde vložte filtr pole", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "od {0} do {1}", diff --git a/packages/devextreme/js/localization/messages/da.json b/packages/devextreme/js/localization/messages/da.json index dcaf4c5437e3..e8744ef60607 100644 --- a/packages/devextreme/js/localization/messages/da.json +++ b/packages/devextreme/js/localization/messages/da.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Slip filterfelter her", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "fra {0} til {1}", diff --git a/packages/devextreme/js/localization/messages/de.json b/packages/devextreme/js/localization/messages/de.json index 9d34fedf0b77..da189f64d9a2 100644 --- a/packages/devextreme/js/localization/messages/de.json +++ b/packages/devextreme/js/localization/messages/de.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Filterfelder hierher ziehen", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "von {0} bis {1}", diff --git a/packages/devextreme/js/localization/messages/el.json b/packages/devextreme/js/localization/messages/el.json index 2a0291976ece..2eb026325056 100644 --- a/packages/devextreme/js/localization/messages/el.json +++ b/packages/devextreme/js/localization/messages/el.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Τοποθετήστε τα πεδία φίλτρων εδώ", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "από {0} έως {1}", diff --git a/packages/devextreme/js/localization/messages/en.json b/packages/devextreme/js/localization/messages/en.json index 600c85c6f71a..d2a75091b633 100644 --- a/packages/devextreme/js/localization/messages/en.json +++ b/packages/devextreme/js/localization/messages/en.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Drop Filter Fields Here", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "from {0} to {1}", diff --git a/packages/devextreme/js/localization/messages/es.json b/packages/devextreme/js/localization/messages/es.json index 96636b8ad054..63cca532cafa 100644 --- a/packages/devextreme/js/localization/messages/es.json +++ b/packages/devextreme/js/localization/messages/es.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Arrastra campos de filtro aquí", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "desde {0} hasta {1}", diff --git a/packages/devextreme/js/localization/messages/fa.json b/packages/devextreme/js/localization/messages/fa.json index 8c69074ae12b..84747c34cc15 100644 --- a/packages/devextreme/js/localization/messages/fa.json +++ b/packages/devextreme/js/localization/messages/fa.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "فیلدهای مربوط به فیلتر را به اینجا انتقال دهید (درگ نمایید)", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "از {0} تا {1}", diff --git a/packages/devextreme/js/localization/messages/fi.json b/packages/devextreme/js/localization/messages/fi.json index 859c86c87f35..9359c4170f35 100644 --- a/packages/devextreme/js/localization/messages/fi.json +++ b/packages/devextreme/js/localization/messages/fi.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Pudota suodatinkentät tähän", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "{0}–{1}", diff --git a/packages/devextreme/js/localization/messages/fr.json b/packages/devextreme/js/localization/messages/fr.json index be2247c2687c..3ecbcb998195 100644 --- a/packages/devextreme/js/localization/messages/fr.json +++ b/packages/devextreme/js/localization/messages/fr.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Déposer les champs de filtre ici", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "de {0} à {1}", diff --git a/packages/devextreme/js/localization/messages/hi.json b/packages/devextreme/js/localization/messages/hi.json index cf6378cce52e..3f6dc1bfce78 100644 --- a/packages/devextreme/js/localization/messages/hi.json +++ b/packages/devextreme/js/localization/messages/hi.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "फ़िल्टर फ़ील्ड यहां छोड़ें", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "{0} से {1} तक", diff --git a/packages/devextreme/js/localization/messages/hu.json b/packages/devextreme/js/localization/messages/hu.json index 5e8a0690148b..4a941c9f1491 100644 --- a/packages/devextreme/js/localization/messages/hu.json +++ b/packages/devextreme/js/localization/messages/hu.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Húzza a szűrőmezőket ide", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "{0}-tól {1}-ig", diff --git a/packages/devextreme/js/localization/messages/it.json b/packages/devextreme/js/localization/messages/it.json index 921d88720022..159cab37c0ab 100644 --- a/packages/devextreme/js/localization/messages/it.json +++ b/packages/devextreme/js/localization/messages/it.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Trascina qui i campi filtro", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "da {0} a {1}", diff --git a/packages/devextreme/js/localization/messages/ja.json b/packages/devextreme/js/localization/messages/ja.json index 086e3a745449..8e76e2ab0a65 100644 --- a/packages/devextreme/js/localization/messages/ja.json +++ b/packages/devextreme/js/localization/messages/ja.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "フィルター フィールドをこちらへドラッグ&ドロップ", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "{0} から {1} まで", diff --git a/packages/devextreme/js/localization/messages/ko.json b/packages/devextreme/js/localization/messages/ko.json index c4ade42c2f02..4dd8a71874cb 100644 --- a/packages/devextreme/js/localization/messages/ko.json +++ b/packages/devextreme/js/localization/messages/ko.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "여기에 필터 필드를 놓으세요", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "{0}부터 {1}까지", diff --git a/packages/devextreme/js/localization/messages/lt.json b/packages/devextreme/js/localization/messages/lt.json index 1cabd91244c2..d8d0a9a4324e 100644 --- a/packages/devextreme/js/localization/messages/lt.json +++ b/packages/devextreme/js/localization/messages/lt.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Vilkite filtrų laukus čia", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "nuo {0} iki {1}", diff --git a/packages/devextreme/js/localization/messages/lv.json b/packages/devextreme/js/localization/messages/lv.json index a12bab972580..5ac6eecf5aea 100644 --- a/packages/devextreme/js/localization/messages/lv.json +++ b/packages/devextreme/js/localization/messages/lv.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Velciet filtra laukus šeit", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "no {0} līdz {1}", diff --git a/packages/devextreme/js/localization/messages/nb.json b/packages/devextreme/js/localization/messages/nb.json index a9db3cd1f13d..8939e2e28c9f 100644 --- a/packages/devextreme/js/localization/messages/nb.json +++ b/packages/devextreme/js/localization/messages/nb.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Slipp filterfelter her", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "fra {0} til {1}", diff --git a/packages/devextreme/js/localization/messages/nl.json b/packages/devextreme/js/localization/messages/nl.json index 096d4d9b6011..5f975045e198 100644 --- a/packages/devextreme/js/localization/messages/nl.json +++ b/packages/devextreme/js/localization/messages/nl.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Sleep filtervelden hier", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "van {0} tot {1}", diff --git a/packages/devextreme/js/localization/messages/pl.json b/packages/devextreme/js/localization/messages/pl.json index b8ed1ffae65e..10614b63277b 100644 --- a/packages/devextreme/js/localization/messages/pl.json +++ b/packages/devextreme/js/localization/messages/pl.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Przeciągnij pola filtra tutaj", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "od {0} do {1}", diff --git a/packages/devextreme/js/localization/messages/pt-BR.json b/packages/devextreme/js/localization/messages/pt-BR.json index 496a6aa856ef..f9b20ab6eb4e 100644 --- a/packages/devextreme/js/localization/messages/pt-BR.json +++ b/packages/devextreme/js/localization/messages/pt-BR.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Arraste os campos de filtro até aqui", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "de {0} a {1}", diff --git a/packages/devextreme/js/localization/messages/pt-PT.json b/packages/devextreme/js/localization/messages/pt-PT.json index 7a51c9f2ac53..533d65c9bd15 100644 --- a/packages/devextreme/js/localization/messages/pt-PT.json +++ b/packages/devextreme/js/localization/messages/pt-PT.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Arraste os campos de filtro até aqui", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "de {0} a {1}", diff --git a/packages/devextreme/js/localization/messages/pt.json b/packages/devextreme/js/localization/messages/pt.json index 940a30b7502b..9978032f4b04 100644 --- a/packages/devextreme/js/localization/messages/pt.json +++ b/packages/devextreme/js/localization/messages/pt.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Arraste os campos de filtro até aqui", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "de {0} a {1}", diff --git a/packages/devextreme/js/localization/messages/ro.json b/packages/devextreme/js/localization/messages/ro.json index c973681f639f..7190c2cb9eec 100644 --- a/packages/devextreme/js/localization/messages/ro.json +++ b/packages/devextreme/js/localization/messages/ro.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Plasați câmpurile de filtrare aici", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "de la {0} până la {1}", diff --git a/packages/devextreme/js/localization/messages/ru.json b/packages/devextreme/js/localization/messages/ru.json index 27b00e1adf60..afc4286a40db 100644 --- a/packages/devextreme/js/localization/messages/ru.json +++ b/packages/devextreme/js/localization/messages/ru.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Перетащите поля фильтров cюда", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "с {0} по {1}", diff --git a/packages/devextreme/js/localization/messages/sl.json b/packages/devextreme/js/localization/messages/sl.json index f49b686be821..4e86689e08b2 100644 --- a/packages/devextreme/js/localization/messages/sl.json +++ b/packages/devextreme/js/localization/messages/sl.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Povleci filtre tukaj", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "od {0} do {1}", diff --git a/packages/devextreme/js/localization/messages/sv.json b/packages/devextreme/js/localization/messages/sv.json index 267b4ff95f4d..ccb82ae633fa 100644 --- a/packages/devextreme/js/localization/messages/sv.json +++ b/packages/devextreme/js/localization/messages/sv.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Släpp filterfält här", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "från {0} till {1}", diff --git a/packages/devextreme/js/localization/messages/tr.json b/packages/devextreme/js/localization/messages/tr.json index c7469e5b211d..1e35f4886024 100644 --- a/packages/devextreme/js/localization/messages/tr.json +++ b/packages/devextreme/js/localization/messages/tr.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Filtre Alanlarını Buraya Bırak", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "{0} - {1}", diff --git a/packages/devextreme/js/localization/messages/uk.json b/packages/devextreme/js/localization/messages/uk.json index 95f8529862bd..ddac2bfc0809 100644 --- a/packages/devextreme/js/localization/messages/uk.json +++ b/packages/devextreme/js/localization/messages/uk.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Перетягніть поля фільтрації сюди", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "з {0} по {1}", diff --git a/packages/devextreme/js/localization/messages/vi.json b/packages/devextreme/js/localization/messages/vi.json index 59e701cfde0b..1e26061d81e6 100644 --- a/packages/devextreme/js/localization/messages/vi.json +++ b/packages/devextreme/js/localization/messages/vi.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "Thả bộ lọc các trường vào đây", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "từ {0} đến {1}", diff --git a/packages/devextreme/js/localization/messages/zh-CN.json b/packages/devextreme/js/localization/messages/zh-CN.json index 6c57f39d4314..14f6ae69ca3c 100644 --- a/packages/devextreme/js/localization/messages/zh-CN.json +++ b/packages/devextreme/js/localization/messages/zh-CN.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "拖动筛选字段到此处", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "从 {0} 到 {1}", diff --git a/packages/devextreme/js/localization/messages/zh-Hans.json b/packages/devextreme/js/localization/messages/zh-Hans.json index b760713eef61..5eaf1ae8822f 100644 --- a/packages/devextreme/js/localization/messages/zh-Hans.json +++ b/packages/devextreme/js/localization/messages/zh-Hans.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "拖动筛选字段到此处", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "从 {0} 到 {1}", diff --git a/packages/devextreme/js/localization/messages/zh-tw.json b/packages/devextreme/js/localization/messages/zh-tw.json index 8cffe465b044..d9ca42213791 100644 --- a/packages/devextreme/js/localization/messages/zh-tw.json +++ b/packages/devextreme/js/localization/messages/zh-tw.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "拖動篩選欄位到此處", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "從 {0} 到 {1}", diff --git a/packages/devextreme/js/localization/messages/zh.json b/packages/devextreme/js/localization/messages/zh.json index 1af2b7ffa1bb..32c5ebf3baaa 100644 --- a/packages/devextreme/js/localization/messages/zh.json +++ b/packages/devextreme/js/localization/messages/zh.json @@ -286,7 +286,7 @@ "dxPivotGrid-filterFieldArea": "拖动筛选字段到此处", "dxPivotGrid-ariaFieldLabel": "Field: {0}", "dxPivotGrid-ariaFieldHeaderFilterLabel": "Header filter applied", - "dxPivotGrid-ariaFieldISortAscLabel": "Sort order: ascending", + "dxPivotGrid-ariaFieldSortAscLabel": "Sort order: ascending", "dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending", "dxScheduler-dateRange": "从 {0} 到 {1}", From 745ab500000a37fe4471ca0ff131a351acc8d1a7 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 17 Jul 2026 12:49:54 -0300 Subject: [PATCH 03/12] PivotGrid - KBN - Reset stale menubar role on hidden field areas --- .../__internal/grids/pivot_grid/fields_area/m_fields_area.ts | 5 +++++ 1 file changed, 5 insertions(+) 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 7ca2d455c2e9..5f2ce5e9afc6 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 @@ -159,6 +159,11 @@ 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'); return; } From 81b084a0d3df7ce9de9899bf7dcc51a7563cca9c Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 17 Jul 2026 13:31:52 -0300 Subject: [PATCH 04/12] PivotGrid - Add axe accessibility tests --- .../tests/accessibility/pivotGrid.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts 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())); From 827e285516cc05fbd6bc41ea0e7803f0ba7506f7 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 17 Jul 2026 13:31:56 -0300 Subject: [PATCH 05/12] PivotGrid - Describe field area keyboard interactions via aria-description --- .../pivot_grid/field_chooser/a11y.test.ts | 33 ++++++++++++------- .../grids/pivot_grid/field_chooser/a11y.ts | 4 +++ .../field_chooser/m_field_chooser.ts | 4 ++- .../pivot_grid/fields_area/m_fields_area.ts | 10 ++++-- .../js/localization/messages/ar.json | 1 + .../js/localization/messages/bg.json | 1 + .../js/localization/messages/ca.json | 1 + .../js/localization/messages/cs.json | 1 + .../js/localization/messages/da.json | 1 + .../js/localization/messages/de.json | 1 + .../js/localization/messages/el.json | 1 + .../js/localization/messages/en.json | 1 + .../js/localization/messages/es.json | 1 + .../js/localization/messages/fa.json | 1 + .../js/localization/messages/fi.json | 1 + .../js/localization/messages/fr.json | 1 + .../js/localization/messages/hi.json | 1 + .../js/localization/messages/hu.json | 1 + .../js/localization/messages/it.json | 1 + .../js/localization/messages/ja.json | 1 + .../js/localization/messages/ko.json | 1 + .../js/localization/messages/lt.json | 1 + .../js/localization/messages/lv.json | 1 + .../js/localization/messages/nb.json | 1 + .../js/localization/messages/nl.json | 1 + .../js/localization/messages/pl.json | 1 + .../js/localization/messages/pt-BR.json | 1 + .../js/localization/messages/pt-PT.json | 1 + .../js/localization/messages/pt.json | 1 + .../js/localization/messages/ro.json | 1 + .../js/localization/messages/ru.json | 1 + .../js/localization/messages/sl.json | 1 + .../js/localization/messages/sv.json | 1 + .../js/localization/messages/tr.json | 1 + .../js/localization/messages/uk.json | 1 + .../js/localization/messages/vi.json | 1 + .../js/localization/messages/zh-CN.json | 1 + .../js/localization/messages/zh-Hans.json | 1 + .../js/localization/messages/zh-tw.json | 1 + .../js/localization/messages/zh.json | 1 + .../pivotGrid.markup.tests.js | 2 ++ 41 files changed, 73 insertions(+), 16 deletions(-) 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 index 2aa7ec1782c8..cbd896a3d203 100644 --- 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 @@ -3,21 +3,24 @@ import { } from '@jest/globals'; import messageLocalization from '@js/common/core/localization/message'; -import { getFieldItemA11yLabel } from './a11y'; +import { getFieldItemA11yLabel, getFieldsAreaA11yDescription } from './a11y'; -describe('getFieldItemA11yLabel', () => { - 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', - }, - }); +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'); }); @@ -42,3 +45,9 @@ describe('getFieldItemA11yLabel', () => { .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 index ba96debfe86b..34e3424d1f76 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts @@ -8,6 +8,7 @@ const I18N_KEYS = { hasHeaderFilter: 'dxPivotGrid-ariaFieldHeaderFilterLabel', sortingAsc: 'dxPivotGrid-ariaFieldSortAscLabel', sortingDesc: 'dxPivotGrid-ariaFieldSortDescLabel', + fieldsAreaDescription: 'dxPivotGrid-ariaFieldsAreaDescription', }; const I18N_MESSAGE_SEPARATOR = ', '; @@ -28,6 +29,9 @@ const getSortingLabel = (sortOrder?: SortOrderType): string | null => { } }; +export const getFieldsAreaA11yDescription = (): string => messageLocalization + .format(I18N_KEYS.fieldsAreaDescription); + export const getFieldItemA11yLabel = ( caption: string, { sortOrder, hasHeaderFilterValue }: FieldItemA11yOptions, 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 b045f3620481..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'; @@ -554,7 +555,8 @@ export class FieldChooser extends FieldChooserBase { const hasFields = !!$container.children().length; $container .attr('role', hasFields ? 'menubar' : null) - .attr('aria-label', hasFields ? that.option(`texts.${area}Fields`) : 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/fields_area/m_fields_area.ts b/packages/devextreme/js/__internal/grids/pivot_grid/fields_area/m_fields_area.ts index 5f2ce5e9afc6..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 = '
'; @@ -163,7 +164,8 @@ class FieldsArea extends AreaItem { // render, so it is reset even though the hidden area skips rendering. tableElement .attr('role', 'presentation') - .removeAttr('aria-label'); + .removeAttr('aria-label') + .removeAttr('aria-description'); return; } @@ -188,7 +190,8 @@ class FieldsArea extends AreaItem { row.attr('role', 'presentation'); tableElement .attr('role', 'menubar') - .attr('aria-label', this._getAreaLabel()); + .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 @@ -196,7 +199,8 @@ class FieldsArea extends AreaItem { // re-renders. tableElement .attr('role', 'presentation') - .removeAttr('aria-label'); + .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/localization/messages/ar.json b/packages/devextreme/js/localization/messages/ar.json index a4fc5ae8d3cc..1494f4550c7b 100644 --- a/packages/devextreme/js/localization/messages/ar.json +++ b/packages/devextreme/js/localization/messages/ar.json @@ -288,6 +288,7 @@ "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 5e2996351015..5471b88035de 100644 --- a/packages/devextreme/js/localization/messages/bg.json +++ b/packages/devextreme/js/localization/messages/bg.json @@ -288,6 +288,7 @@ "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 b51a0f2af8e9..b3ad530a7371 100644 --- a/packages/devextreme/js/localization/messages/ca.json +++ b/packages/devextreme/js/localization/messages/ca.json @@ -288,6 +288,7 @@ "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 799b44364db5..65d9167147de 100644 --- a/packages/devextreme/js/localization/messages/cs.json +++ b/packages/devextreme/js/localization/messages/cs.json @@ -288,6 +288,7 @@ "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 e8744ef60607..beed4beca18b 100644 --- a/packages/devextreme/js/localization/messages/da.json +++ b/packages/devextreme/js/localization/messages/da.json @@ -288,6 +288,7 @@ "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 da189f64d9a2..000e35e50d45 100644 --- a/packages/devextreme/js/localization/messages/de.json +++ b/packages/devextreme/js/localization/messages/de.json @@ -288,6 +288,7 @@ "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 2eb026325056..2dea7899684c 100644 --- a/packages/devextreme/js/localization/messages/el.json +++ b/packages/devextreme/js/localization/messages/el.json @@ -288,6 +288,7 @@ "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 d2a75091b633..cd72599595bf 100644 --- a/packages/devextreme/js/localization/messages/en.json +++ b/packages/devextreme/js/localization/messages/en.json @@ -288,6 +288,7 @@ "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 63cca532cafa..8dfae2975cf1 100644 --- a/packages/devextreme/js/localization/messages/es.json +++ b/packages/devextreme/js/localization/messages/es.json @@ -288,6 +288,7 @@ "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 84747c34cc15..a1c5a186a7c7 100644 --- a/packages/devextreme/js/localization/messages/fa.json +++ b/packages/devextreme/js/localization/messages/fa.json @@ -288,6 +288,7 @@ "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 9359c4170f35..65cc2ee5d63e 100644 --- a/packages/devextreme/js/localization/messages/fi.json +++ b/packages/devextreme/js/localization/messages/fi.json @@ -288,6 +288,7 @@ "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 3ecbcb998195..6fd233c42e21 100644 --- a/packages/devextreme/js/localization/messages/fr.json +++ b/packages/devextreme/js/localization/messages/fr.json @@ -288,6 +288,7 @@ "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 3f6dc1bfce78..8d5b3aff28fb 100644 --- a/packages/devextreme/js/localization/messages/hi.json +++ b/packages/devextreme/js/localization/messages/hi.json @@ -288,6 +288,7 @@ "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 4a941c9f1491..7b3965d86af5 100644 --- a/packages/devextreme/js/localization/messages/hu.json +++ b/packages/devextreme/js/localization/messages/hu.json @@ -288,6 +288,7 @@ "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 159cab37c0ab..044cffdbcdb8 100644 --- a/packages/devextreme/js/localization/messages/it.json +++ b/packages/devextreme/js/localization/messages/it.json @@ -288,6 +288,7 @@ "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 8e76e2ab0a65..69dd7c165412 100644 --- a/packages/devextreme/js/localization/messages/ja.json +++ b/packages/devextreme/js/localization/messages/ja.json @@ -288,6 +288,7 @@ "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 4dd8a71874cb..b6c7d3c0ad1b 100644 --- a/packages/devextreme/js/localization/messages/ko.json +++ b/packages/devextreme/js/localization/messages/ko.json @@ -288,6 +288,7 @@ "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 d8d0a9a4324e..3e22b1d98c44 100644 --- a/packages/devextreme/js/localization/messages/lt.json +++ b/packages/devextreme/js/localization/messages/lt.json @@ -288,6 +288,7 @@ "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 5ac6eecf5aea..ca52ca02489c 100644 --- a/packages/devextreme/js/localization/messages/lv.json +++ b/packages/devextreme/js/localization/messages/lv.json @@ -288,6 +288,7 @@ "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 8939e2e28c9f..8059316faff5 100644 --- a/packages/devextreme/js/localization/messages/nb.json +++ b/packages/devextreme/js/localization/messages/nb.json @@ -288,6 +288,7 @@ "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 5f975045e198..19b577b2f8fc 100644 --- a/packages/devextreme/js/localization/messages/nl.json +++ b/packages/devextreme/js/localization/messages/nl.json @@ -288,6 +288,7 @@ "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 10614b63277b..606db7fd3436 100644 --- a/packages/devextreme/js/localization/messages/pl.json +++ b/packages/devextreme/js/localization/messages/pl.json @@ -288,6 +288,7 @@ "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 f9b20ab6eb4e..187301ce3c46 100644 --- a/packages/devextreme/js/localization/messages/pt-BR.json +++ b/packages/devextreme/js/localization/messages/pt-BR.json @@ -288,6 +288,7 @@ "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 533d65c9bd15..c65b214e866b 100644 --- a/packages/devextreme/js/localization/messages/pt-PT.json +++ b/packages/devextreme/js/localization/messages/pt-PT.json @@ -288,6 +288,7 @@ "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 9978032f4b04..c2d4c9acd648 100644 --- a/packages/devextreme/js/localization/messages/pt.json +++ b/packages/devextreme/js/localization/messages/pt.json @@ -288,6 +288,7 @@ "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 7190c2cb9eec..5952a344d34f 100644 --- a/packages/devextreme/js/localization/messages/ro.json +++ b/packages/devextreme/js/localization/messages/ro.json @@ -288,6 +288,7 @@ "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 afc4286a40db..cf99f090ca53 100644 --- a/packages/devextreme/js/localization/messages/ru.json +++ b/packages/devextreme/js/localization/messages/ru.json @@ -288,6 +288,7 @@ "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 4e86689e08b2..642b37fe61a9 100644 --- a/packages/devextreme/js/localization/messages/sl.json +++ b/packages/devextreme/js/localization/messages/sl.json @@ -288,6 +288,7 @@ "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 ccb82ae633fa..bf5bcace26aa 100644 --- a/packages/devextreme/js/localization/messages/sv.json +++ b/packages/devextreme/js/localization/messages/sv.json @@ -288,6 +288,7 @@ "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 1e35f4886024..be576db493a4 100644 --- a/packages/devextreme/js/localization/messages/tr.json +++ b/packages/devextreme/js/localization/messages/tr.json @@ -288,6 +288,7 @@ "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 ddac2bfc0809..27af6ea144aa 100644 --- a/packages/devextreme/js/localization/messages/uk.json +++ b/packages/devextreme/js/localization/messages/uk.json @@ -288,6 +288,7 @@ "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 1e26061d81e6..aa8e4dac9760 100644 --- a/packages/devextreme/js/localization/messages/vi.json +++ b/packages/devextreme/js/localization/messages/vi.json @@ -288,6 +288,7 @@ "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 14f6ae69ca3c..9744677d9377 100644 --- a/packages/devextreme/js/localization/messages/zh-CN.json +++ b/packages/devextreme/js/localization/messages/zh-CN.json @@ -288,6 +288,7 @@ "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 5eaf1ae8822f..78df5e0e447f 100644 --- a/packages/devextreme/js/localization/messages/zh-Hans.json +++ b/packages/devextreme/js/localization/messages/zh-Hans.json @@ -288,6 +288,7 @@ "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 d9ca42213791..0fd718756c76 100644 --- a/packages/devextreme/js/localization/messages/zh-tw.json +++ b/packages/devextreme/js/localization/messages/zh-tw.json @@ -288,6 +288,7 @@ "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 32c5ebf3baaa..63bed7ec40b7 100644 --- a/packages/devextreme/js/localization/messages/zh.json +++ b/packages/devextreme/js/localization/messages/zh.json @@ -288,6 +288,7 @@ "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 eb4019396c83..2018b9dbc554 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 @@ -271,6 +271,7 @@ QUnit.module('PivotGrid accessibility markup', { assert.strictEqual($table.length, 1, `${area} field table exists`); 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 @@ -280,6 +281,7 @@ QUnit.module('PivotGrid accessibility markup', { 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) { From 089cf9713d53af23d9883f2f0ca832c8cae036d7 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 17 Jul 2026 13:32:14 -0300 Subject: [PATCH 06/12] PivotGrid - Open field context menu from keyboard --- .../tests/common/pivotGrid/kbn/fields.ts | 53 ++++++++++++++++++- .../field_chooser/m_field_chooser_base.ts | 29 ++++++++++ .../__internal/grids/pivot_grid/m_widget.ts | 15 +++++- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts index b153dfce0b37..82c00f69979d 100644 --- a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts +++ b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts @@ -127,7 +127,9 @@ const createConfig = () => ({ .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'); + .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')) @@ -405,3 +407,52 @@ 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())); + +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') + .pressKey('shift+f10') + .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) + .pressKey('shift+f10') + .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) + .pressKey('shift+f10') + .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/field_chooser/m_field_chooser_base.ts b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/m_field_chooser_base.ts index 1fbd0eed1d7f..3d06f7b11384 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 @@ -48,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; @@ -127,6 +132,7 @@ export class FieldChooserBase extends mixinWidget { allowFieldDragging: true, applyChangesMode: 'instantly', state: null, + onFieldContextMenuKeyDown: null, headerFilter: { width: 252, height: 325, @@ -424,6 +430,11 @@ export class FieldChooserBase extends mixinWidget { return; } + if (isContextMenuKeyEvent(e)) { + this._handleFieldContextMenuKey(e, field); + return; + } + if (!isClick) { return; } @@ -555,6 +566,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/m_widget.ts b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts index f122d1881117..d664a42739c0 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -1053,7 +1053,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 +1461,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) { From 982706dd39ad84e2805f1f3b3374bca1e4192a3b Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 17 Jul 2026 13:53:10 -0300 Subject: [PATCH 07/12] PivotGrid - Handle the context menu handler option change and dispatch Shift+F10 in e2e --- .../tests/common/pivotGrid/kbn/fields.ts | 12 +++++++++--- .../pivot_grid/field_chooser/m_field_chooser_base.ts | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts index 82c00f69979d..011f13651e09 100644 --- a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts +++ b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts @@ -408,6 +408,12 @@ test('FieldChooser: Should traverse fields in all areas by tab', async (t) => { .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); @@ -417,7 +423,7 @@ test('PivotGrid: Should open the context menu by Shift+F10 on a field', async (t .click(firstField) .expect(firstField.focused) .ok('field is focused after click') - .pressKey('shift+f10') + .dispatchEvent(firstField, 'keydown', SHIFT_F10_KEYDOWN) .expect(contextMenuItem.visible) .ok('the field context menu is shown after Shift+F10'); }).before(async () => createWidget('dxPivotGrid', createConfig())); @@ -429,7 +435,7 @@ test('PivotGrid: Field should have focus after the context menu is closed', asyn await t .click(firstField) - .pressKey('shift+f10') + .dispatchEvent(firstField, 'keydown', SHIFT_F10_KEYDOWN) .expect(contextMenuItem.visible) .ok('the field context menu is shown after Shift+F10'); @@ -452,7 +458,7 @@ test('FieldChooser: Shift+F10 on a popup field should not open the grid context // grid context menu either. await t .click(firstField) - .pressKey('shift+f10') + .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/field_chooser/m_field_chooser_base.ts b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/m_field_chooser_base.ts index 3d06f7b11384..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 @@ -193,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) { From 1a468919252528895685634f91ad43a097e88293 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 17 Jul 2026 19:07:07 -0300 Subject: [PATCH 08/12] PivotGrid - Describe data cells with their row and column headers --- .../a11y/data_cell_description.test.ts | 115 ++++++++++++++++++ .../pivot_grid/a11y/data_cell_description.ts | 101 +++++++++++++++ .../__internal/grids/pivot_grid/m_widget.ts | 7 ++ .../pivotGrid.markup.tests.js | 43 +++++++ 4 files changed, 266 insertions(+) create mode 100644 packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.test.ts create mode 100644 packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.ts 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
102030
AfricaCairo
Luxor
10
20
2015
Africa
10
20152016
1020
20152016
1020
10
2015Grand Total
Q1Q2
102030
AfricaCairo
Luxor
10
20
2015
Africa
10
20152016
1020
20152016
1020
10
').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 3e8a2c750766..d664a42739c0 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -26,7 +26,6 @@ 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'; @@ -1519,14 +1518,6 @@ 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/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.markup.tests.js index 509f11554632..aa0407749f27 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 @@ -270,8 +270,9 @@ QUnit.module('PivotGrid accessibility markup', { assert.strictEqual($table.length, 1, `${area} field table exists`); 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`); + assert.strictEqual($table.attr('aria-label').indexOf(expectedLabels[area]), 0, `${area} menubar label starts with the area name`); + assert.ok($table.attr('aria-label').length > expectedLabels[area].length, `${area} menubar label also carries the keyboard instructions`); + assert.strictEqual($table.attr('aria-description'), undefined, `${area} menubar does not use the unsupported aria-description attribute`); }); // The filter area has no fields in this data source; a menubar without @@ -281,7 +282,6 @@ QUnit.module('PivotGrid accessibility markup', { 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) { @@ -339,49 +339,6 @@ 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); From 06e0710cd7ea8e500867cd7e2ac81fefde84e049 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 23 Jul 2026 11:17:39 -0300 Subject: [PATCH 11/12] PivotGrid - Stabilize axe a11y tests and guard empty field-area caption --- .../tests/accessibility/pivotGrid.ts | 10 ++++++++++ .../grids/pivot_grid/field_chooser/a11y.test.ts | 5 +++++ .../__internal/grids/pivot_grid/field_chooser/a11y.ts | 7 ++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts b/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts index d5a9dc3b1b02..aeaa6360b85f 100644 --- a/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts +++ b/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts @@ -82,6 +82,10 @@ test('field chooser popup', async (t) => { await t.click(pivotGrid.getFieldChooserButton()); + // The popup opens with an animation; wait for its content to settle so axe + // does not sample a transient mid-animation state. + await t.expect(Selector('.dx-fieldchooser-popup .dx-pivotgridfieldchooser').visible).ok(); + await a11yCheck(t); }).before(async () => createWidget('dxPivotGrid', createConfig())); @@ -94,6 +98,8 @@ test('header filter popup', async (t) => { await t.click(filterIcon); + await t.expect(Selector('.dx-header-filter-menu .dx-list-item').visible).ok(); + await a11yCheck(t); }).before(async () => createWidget('dxPivotGrid', createConfig())); @@ -102,5 +108,9 @@ test('cell context menu', async (t) => { await t.rightClick(columnHeaderCell); + // The context menu is an animated overlay; wait for its items to be visible + // before running axe to avoid flaky mid-animation samples. + await t.expect(Selector('.dx-context-menu .dx-menu-item').visible).ok(); + await a11yCheck(t); }).before(async () => createWidget('dxPivotGrid', createConfig())); 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 index 864005ff611d..6d2ef1cf3155 100644 --- 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 @@ -56,4 +56,9 @@ describe('getFieldsAreaA11yLabel', () => { it('should compose the area name with the interaction description', () => { expect(getFieldsAreaA11yLabel('Row Fields')).toBe(`Row Fields. ${AREA_DESCRIPTION}`); }); + + it('should fall back to the description alone when the area name is empty', () => { + expect(getFieldsAreaA11yLabel(undefined)).toBe(AREA_DESCRIPTION); + expect(getFieldsAreaA11yLabel('')).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 index 419d85b2fea1..21b7d24160b9 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/field_chooser/a11y.ts @@ -36,7 +36,12 @@ export const getFieldsAreaA11yDescription = (): string => messageLocalization // exposed via aria-description: aria-description is an ARIA 1.3 draft attribute // that screen readers do not read reliably, whereas the label composition // mirrors how DataGrid/TreeList surface such instructions. -export const getFieldsAreaA11yLabel = (areaLabel: string): string => `${areaLabel}. ${getFieldsAreaA11yDescription()}`; +export const getFieldsAreaA11yLabel = (areaLabel?: string): string => { + const description = getFieldsAreaA11yDescription(); + // A custom (public, optional) area caption may be empty; fall back to the + // description alone rather than composing "null. …"/"undefined. …". + return areaLabel ? `${areaLabel}. ${description}` : description; +}; export const getFieldItemA11yLabel = ( caption: string, From 1835da0100b2dfee41f1ebd9ad63f81a69c485d6 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 23 Jul 2026 11:33:49 -0300 Subject: [PATCH 12/12] PivotGrid - Disable best-practice region rule for overlay axe scenarios --- .../tests/accessibility/pivotGrid.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts b/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts index aeaa6360b85f..832980a06f49 100644 --- a/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts +++ b/e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts @@ -77,16 +77,18 @@ test('grid with field panel', async (t) => { await a11yCheck(t, {}, PIVOT_GRID_SELECTOR); }).before(async () => createWidget('dxPivotGrid', createConfig())); +// The popup/menu widgets are portaled to , outside the harness page +// landmarks, so the best-practice 'region' rule fires on them regardless of +// the widget's own accessibility (same convention as dataGrid/cardView a11y +// tests). It is disabled for the overlay scenarios. +const overlayA11yOptions = { rules: { region: { enabled: false } } }; + test('field chooser popup', async (t) => { const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); await t.click(pivotGrid.getFieldChooserButton()); - // The popup opens with an animation; wait for its content to settle so axe - // does not sample a transient mid-animation state. - await t.expect(Selector('.dx-fieldchooser-popup .dx-pivotgridfieldchooser').visible).ok(); - - await a11yCheck(t); + await a11yCheck(t, overlayA11yOptions); }).before(async () => createWidget('dxPivotGrid', createConfig())); test('header filter popup', async (t) => { @@ -98,9 +100,10 @@ test('header filter popup', async (t) => { await t.click(filterIcon); + // Wait for the popup list to render so axe audits the fully-open popup. await t.expect(Selector('.dx-header-filter-menu .dx-list-item').visible).ok(); - await a11yCheck(t); + await a11yCheck(t, overlayA11yOptions); }).before(async () => createWidget('dxPivotGrid', createConfig())); test('cell context menu', async (t) => { @@ -108,9 +111,8 @@ test('cell context menu', async (t) => { await t.rightClick(columnHeaderCell); - // The context menu is an animated overlay; wait for its items to be visible - // before running axe to avoid flaky mid-animation samples. + // Wait for the menu items so axe audits the fully-open context menu. await t.expect(Selector('.dx-context-menu .dx-menu-item').visible).ok(); - await a11yCheck(t); + await a11yCheck(t, overlayA11yOptions); }).before(async () => createWidget('dxPivotGrid', createConfig()));