Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/fields.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
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({
Comment thread
aleksei-semikozov marked this conversation as resolved.
en: {
'dxPivotGrid-ariaFieldLabel': 'Field: {0}',
'dxPivotGrid-ariaFieldHeaderFilterLabel': 'Header filter applied',
'dxPivotGrid-ariaFieldSortAscLabel': '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-ariaFieldSortAscLabel',
Comment thread
aleksei-semikozov marked this conversation as resolved.
sortingDesc: 'dxPivotGrid-ariaFieldSortDescLabel',
};
Comment thread
aleksei-semikozov marked this conversation as resolved.

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),
Comment thread
aleksei-semikozov marked this conversation as resolved.
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 @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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?) {
}
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());
Comment thread
aleksei-semikozov marked this conversation as resolved.
Comment thread
aleksei-semikozov marked this conversation as resolved.
} 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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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-ariaFieldSortAscLabel": "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/hi.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-ariaFieldSortAscLabel": "Sort order: ascending",
"dxPivotGrid-ariaFieldSortDescLabel": "Sort order: descending",

"dxScheduler-dateRange": "{0} से {1} तक",
"dxScheduler-ariaLabel": "अनुसूचक. {0} दृश्य: {1} {2} नियुक्तियों के साथ",
Expand Down
Loading
Loading