Skip to content
24 changes: 24 additions & 0 deletions e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,30 @@ const createConfig = () => ({
? Selector(`.dx-pivotgridfieldchooser .dx-area-fields[group="${area}"]`)
: Selector(`.dx-pivotgrid-fields-area[group="${area}"]`));

test(`${testTitlePrefix}: Fields should be exposed as menu items of an area menubar`, async (t) => {
const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR);

if (isFieldChooser) {
await t.click(pivotGrid.getFieldChooserButton());
}

const rowAreaContainer = getAreaFieldsContainer('row');
const menubar = rowAreaContainer.find('[role="menubar"]');
const firstField = getField(pivotGrid, 'row', 0);

await t
.expect(menubar.count)
.eql(1, 'the area has a single menubar')
.expect(menubar.getAttribute('aria-label'))
.eql('Row Fields', 'the menubar is labelled with the area name');

await t
.expect(firstField.getAttribute('role'))
.eql('menuitem', 'a field is exposed as a menu item')
.expect(firstField.getAttribute('aria-label'))
.eql('Field: Region, Sort order: ascending', 'the field label includes the name and the sorting state');
}).before(async () => createWidget('dxPivotGrid', createConfig()));

['filter', 'data', 'column', 'row'].forEach((area) => {
test(`${testTitlePrefix}: Fields in ${area} area should form a single tab stop`, async (t) => {
const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR);
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import type { RovingTabIndexComponent } from '../keyboard_navigation/roving_tab_
import { RovingTabIndex } from '../keyboard_navigation/roving_tab_index';
import { createPath, foreachTree } from '../m_widget_utils';
import SortableModule from '../sortable/m_sortable';
import { getFieldItemA11yLabel } from './a11y';
import type { SortOrderType } from './const';
import { ATTRIBUTES, CLASSES } from './const';
import { dragAndDropItemRender } from './dom';
import { reverseSortOrder, shouldCancelDragging } from './utils';
Expand Down Expand Up @@ -242,9 +244,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?) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,20 @@ class FieldsArea extends AreaItem {
.attr('group', this._area);
}

_createTableElement() {
_getAreaLabel() {
const localizationMessageMap = {
row: 'dxPivotGrid-rowFields',
column: 'dxPivotGrid-columnFields',
data: 'dxPivotGrid-dataFields',
filter: 'dxPivotGrid-filterFields',
};

return localizationMessage.format(localizationMessageMap[this._area]);
}

_createTableElement() {
return $('<table>')
.attr('role', 'group')
.attr('aria-label', localizationMessage.format(localizationMessageMap[this._area]));
.attr('role', 'presentation');
}

isVisible() {
Expand Down Expand Up @@ -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 = $('<thead>').addClass('dx-pivotgrid-fields-area-head').appendTo(tableElement);
const head = $('<thead>')
.addClass('dx-pivotgrid-fields-area-head')
.attr('role', 'presentation')
.appendTo(tableElement);
const area = that._area;
const row = $('<tr>');

Expand All @@ -158,7 +164,9 @@ class FieldsArea extends AreaItem {

each(data, (index: number, field) => {
if (field.area === area && field.visible !== false) {
const td = $('<td>').append(fieldChooserBase.renderField(field, field.area === 'row'));
const td = $('<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());
Expand All @@ -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');
$('<td>').append($(DIV).addClass('dx-empty-area-text').text(this.option(`fieldPanel.texts.${area}FieldArea`))).appendTo(row);
}

Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -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} مواعيد",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -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} срещи",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -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} ραντεβού",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/fa.json
Original file line number Diff line number Diff line change
Expand Up @@ -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} قرار ملاقات",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/localization/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading