Skip to content

Commit 48c60bd

Browse files
authored
DataGrid: Fix the header filter\'s aria-label when column's caption has a "\n" character (T1318766). Normalization of aria attributes of elements. (#32249)
Co-authored-by: Alyar <>
1 parent 552a4f8 commit 48c60bd

File tree

14 files changed

+145
-44
lines changed

14 files changed

+145
-44
lines changed

packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,25 @@ const DATAGRID_CELL_DISABLED = 'dx-cell-focus-disabled';
3737
const DATAGRID_GROUP_FOOTER_ROW_TYPE = 'groupFooter';
3838
const DATAGRID_TOTAL_FOOTER_ROW_TYPE = 'totalFooter';
3939

40-
export const renderSummaryCell = function (cell, options) {
40+
export const renderSummaryCell = function (cell, options, setAria) {
4141
const $cell = $(cell);
4242
const { column } = options;
4343
const { summaryItems } = options;
4444
const $summaryItems: any = [];
4545

4646
if (!column.command && summaryItems) {
47-
for (let i = 0; i < summaryItems.length; i++) {
48-
const summaryItem = summaryItems[i];
47+
for (const summaryItem of summaryItems) {
4948
const text = gridCore.getSummaryText(summaryItem, options.summaryTexts);
50-
51-
$summaryItems.push($('<div>')
49+
const $summaryItemElement = $('<div>')
5250
.css('textAlign', summaryItem.alignment || column.alignment)
5351
.addClass(DATAGRID_SUMMARY_ITEM_CLASS)
5452
.addClass(DATAGRID_TEXT_CONTENT_CLASS)
5553
.addClass(summaryItem.cssClass)
5654
.toggleClass(DATAGRID_GROUP_TEXT_CONTENT_CLASS, options.rowType === 'group')
57-
.text(text)
58-
.attr('aria-label', `${column.caption} ${text}`));
55+
.text(text);
56+
57+
setAria('label', `${column.caption ?? ''} ${text ?? ''}`, $summaryItemElement);
58+
$summaryItems.push($summaryItemElement);
5959
}
6060
$cell.append($summaryItems);
6161
}
@@ -204,7 +204,7 @@ export class FooterView extends ColumnsView {
204204
}
205205

206206
protected _renderCellContent($cell, options) {
207-
renderSummaryCell($cell, options);
207+
renderSummaryCell($cell, options, this.setAria.bind(this));
208208
// @ts-expect-error
209209
super._renderCellContent.apply(this, arguments);
210210
}
@@ -911,7 +911,7 @@ const rowsView = (Base: ModuleType<RowsView>) => class SummaryRowsViewExtender e
911911

912912
protected _getCellTemplate(options) {
913913
if (!options.column.command && !isDefined(options.column.groupIndex) && options.summaryItems && options.summaryItems.length) {
914-
return renderSummaryCell;
914+
return (cell, options) => renderSummaryCell(cell, options, this.setAria.bind(this));
915915
}
916916
return super._getCellTemplate(options);
917917
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {
2+
afterEach, beforeEach, describe, expect, it, jest,
3+
} from '@jest/globals';
4+
import fx from '@js/common/core/animation/fx';
5+
import type { dxElementWrapper } from '@js/core/renderer';
6+
import $ from '@js/core/renderer';
7+
import type { Properties as DataGridProperties } from '@js/ui/data_grid';
8+
import DataGrid from '@js/ui/data_grid';
9+
import { DataGridModel } from '@ts/grids/data_grid/__tests__/__mock__/model/data_grid';
10+
11+
const SELECTORS = {
12+
gridContainer: '#gridContainer',
13+
};
14+
15+
const GRID_CONTAINER_ID = 'gridContainer';
16+
17+
const createDataGrid = async (
18+
options: DataGridProperties = {},
19+
): Promise<{
20+
$container: dxElementWrapper;
21+
component: DataGridModel;
22+
instance: DataGrid;
23+
}> => new Promise((resolve) => {
24+
const $container = $('<div>')
25+
.attr('id', GRID_CONTAINER_ID)
26+
.appendTo(document.body);
27+
28+
const dataGridOptions: DataGridProperties = {
29+
keyExpr: 'id',
30+
...options,
31+
};
32+
33+
const instance = new DataGrid($container.get(0) as HTMLDivElement, dataGridOptions);
34+
const component = new DataGridModel($container.get(0) as HTMLElement);
35+
36+
jest.runAllTimers();
37+
38+
resolve({
39+
$container,
40+
component,
41+
instance,
42+
});
43+
});
44+
45+
const beforeTest = (): void => {
46+
fx.off = true;
47+
jest.useFakeTimers();
48+
};
49+
50+
const afterTest = (): void => {
51+
const $container = $(SELECTORS.gridContainer);
52+
const dataGrid = ($container as any).dxDataGrid('instance') as DataGrid;
53+
54+
dataGrid.dispose();
55+
$container.remove();
56+
jest.clearAllMocks();
57+
jest.useRealTimers();
58+
fx.off = false;
59+
};
60+
61+
describe('Grid', () => {
62+
beforeEach(beforeTest);
63+
afterEach(afterTest);
64+
65+
describe('when column caption has a newline character', () => {
66+
it('should exclude the newline character from the header filter\'s aria-label', async () => {
67+
const { component } = await createDataGrid({
68+
dataSource: [
69+
{ id: 1, name: 'Name 1', value: 10 },
70+
{ id: 2, name: 'Name 2', value: 20 },
71+
],
72+
columns: ['id', { dataField: 'name', caption: 'Test\nName' }, 'value'],
73+
showBorders: true,
74+
headerFilter: {
75+
visible: true,
76+
},
77+
});
78+
79+
expect(component.getHeaderCellFilter(1).attr('aria-label'))
80+
.toBe('Show filter options for column \'Test Name\'');
81+
});
82+
});
83+
});

packages/devextreme/js/__internal/grids/grid_core/adaptivity/m_adaptivity.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,10 @@ export class AdaptiveColumnsController extends modules.ViewController {
849849

850850
public setCommandAdaptiveAriaLabel($row, labelName) {
851851
const $adaptiveCommand = $row.find('.dx-command-adaptive');
852-
$adaptiveCommand.attr('aria-label', messageLocalization.format(labelName));
852+
853+
if ($adaptiveCommand.length) {
854+
this.setAria('label', messageLocalization.format(labelName), $adaptiveCommand);
855+
}
853856
}
854857
}
855858

packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2302,7 +2302,10 @@ class EditingControllerImpl extends modules.ViewController {
23022302
$container.addClass(COMMAND_EDIT_WITH_ICONS_CLASS);
23032303

23042304
const localizationName = this.getButtonLocalizationNames()[button.name];
2305-
localizationName && $button.attr('aria-label', messageLocalization.format(localizationName));
2305+
2306+
if (localizationName) {
2307+
this.setAria('label', messageLocalization.format(localizationName), $button);
2308+
}
23062309
} else {
23072310
$button.text(button.text);
23082311
}

packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ const editingControllerExtender = (Base: ModuleType<EditingController>) => class
135135
}
136136
}
137137

138-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
139138
protected _updateEditRowCore(row, skipCurrentRow, isCustomSetCellValue) {
140139
const editForm = this._editForm;
141140

@@ -219,7 +218,7 @@ const editingControllerExtender = (Base: ModuleType<EditingController>) => class
219218

220219
formTemplate(this._$popupContent, templateOptions, { isPopupForm: true });
221220
this._rowsView.renderDelayedTemplates();
222-
$(container).parent().attr('aria-label', this.localize('dxDataGrid-ariaEditForm'));
221+
this.setAria('label', this.localize('dxDataGrid-ariaEditForm'), $(container).parent());
223222
};
224223
}
225224

packages/devextreme/js/__internal/grids/grid_core/error_handling/m_error_handling.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,12 @@ export class ErrorHandlingController extends modules.ViewController {
8282
private _renderErrorMessage(error) {
8383
const message = error.url ? error.message.replace(error.url, '') : error.message || error;
8484
const $message = $('<div>')
85-
.attr('role', 'alert')
86-
.attr('aria-roledescription', messageLocalization.format('dxDataGrid-ariaError'))
8785
.addClass(ERROR_MESSAGE_CLASS)
8886
.text(message);
8987

88+
this.setAria('role', 'alert', $message);
89+
this.setAria('roledescription', messageLocalization.format('dxDataGrid-ariaError'), $message);
90+
9091
if (error.url) {
9192
$('<a>').attr('href', error.url).text(error.url).appendTo($message);
9293
}

packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -542,9 +542,9 @@ export const headerFilterMixin = <T extends ModuleType<any>>(Base: T) => class H
542542

543543
const indicatorLabel = (messageLocalization.format as any)('dxDataGrid-headerFilterIndicatorLabel', column.caption);
544544

545-
$headerFilterIndicator.attr('aria-label', indicatorLabel);
546-
$headerFilterIndicator.attr('aria-haspopup', 'dialog');
547-
$headerFilterIndicator.attr('role', 'button');
545+
this.setAria('label', indicatorLabel, $headerFilterIndicator);
546+
this.setAria('haspopup', 'dialog', $headerFilterIndicator);
547+
this.setAria('role', 'button', $headerFilterIndicator);
548548
}
549549

550550
return $headerFilterIndicator;

packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ export class HeaderPanel extends ColumnsView {
9393
const $headerPanel = this.element();
9494
$headerPanel.addClass(this.addWidgetPrefix(HEADER_PANEL_CLASS));
9595
const label = messageLocalization.format(this.component.NAME + TOOLBAR_ARIA_LABEL);
96-
const $toolbar = $('<div>').attr('aria-label', label).appendTo($headerPanel);
96+
const $toolbar = $('<div>').appendTo($headerPanel);
97+
98+
this.setAria('label', label, $toolbar);
9799
this._toolbar = this._createComponent($toolbar, Toolbar, this._toolbarOptions);
98100
} else {
99101
this._toolbar.option(this._toolbarOptions!);

packages/devextreme/js/__internal/grids/grid_core/m_modules.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
22
/* eslint-disable max-classes-per-file */
3-
/* eslint-disable @typescript-eslint/method-signature-style */
3+
44
import messageLocalization from '@js/common/core/localization/message';
55
import type { Component } from '@js/core/component';
66
import type { dxElementWrapper } from '@js/core/renderer';
@@ -9,7 +9,7 @@ import Callbacks from '@js/core/utils/callbacks';
99
// @ts-expect-error
1010
import { grep } from '@js/core/utils/common';
1111
import { each } from '@js/core/utils/iterator';
12-
import { isFunction } from '@js/core/utils/type';
12+
import { isDefined, isFunction } from '@js/core/utils/type';
1313
import { hasWindow } from '@js/core/utils/window';
1414
import errors from '@js/ui/widget/ui.errors';
1515

@@ -145,14 +145,23 @@ export class ModuleItem {
145145
return this._actions[actionName];
146146
}
147147

148-
public setAria(name, value, $target) {
148+
public setAria(
149+
name: string,
150+
value: string | number | boolean | undefined,
151+
$target: dxElementWrapper,
152+
) {
153+
if (!isDefined(value)) {
154+
return;
155+
}
156+
149157
const target = $target.get(0);
150158
const prefix = name !== 'role' && name !== 'id' ? 'aria-' : '';
159+
const normalizedValue = String(value).replace(/\s+/g, ' ').trim();
151160

152-
if (target.setAttribute) {
153-
target.setAttribute(prefix + name, value);
161+
if (target?.setAttribute) {
162+
target.setAttribute(prefix + name, normalizedValue);
154163
} else {
155-
$target.attr(prefix + name, value);
164+
$target.attr(prefix + name, normalizedValue);
156165
}
157166
}
158167

@@ -485,9 +494,8 @@ export function processModules(
485494
rootViewTypes,
486495
);
487496

488-
// eslint-disable-next-line no-param-reassign
489497
componentInstance._controllers = createModuleItems(controllerTypes);
490-
// eslint-disable-next-line no-param-reassign
498+
491499
componentInstance._views = createModuleItems(viewTypes);
492500
}
493501

packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ const rowsView = (Base: ModuleType<RowsView>) => class RowsViewMasterDetailExten
396396
const isEditForm = row.isEditing;
397397

398398
if (!isEditForm) {
399-
$detailCell.attr('aria-roledescription', messageLocalization.format('dxDataGrid-masterDetail'));
399+
this.setAria('roledescription', messageLocalization.format('dxDataGrid-masterDetail'), $detailCell);
400400
}
401401

402402
return $detailCell;

0 commit comments

Comments
 (0)