diff --git a/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts b/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts index 6c7fca08f036..eda93e064bcb 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts @@ -37,7 +37,7 @@ const DATAGRID_CELL_DISABLED = 'dx-cell-focus-disabled'; const DATAGRID_GROUP_FOOTER_ROW_TYPE = 'groupFooter'; const DATAGRID_TOTAL_FOOTER_ROW_TYPE = 'totalFooter'; -export const renderSummaryCell = function (cell, options) { +export const renderSummaryCell = function (cell, options, setAria) { const $cell = $(cell); const { column } = options; const { summaryItems } = options; @@ -47,15 +47,16 @@ export const renderSummaryCell = function (cell, options) { for (let i = 0; i < summaryItems.length; i++) { const summaryItem = summaryItems[i]; const text = gridCore.getSummaryText(summaryItem, options.summaryTexts); - - $summaryItems.push($('
') + const $summaryItemElement = $('
') .css('textAlign', summaryItem.alignment || column.alignment) .addClass(DATAGRID_SUMMARY_ITEM_CLASS) .addClass(DATAGRID_TEXT_CONTENT_CLASS) .addClass(summaryItem.cssClass) .toggleClass(DATAGRID_GROUP_TEXT_CONTENT_CLASS, options.rowType === 'group') - .text(text) - .attr('aria-label', `${column.caption} ${text}`)); + .text(text); + + setAria('label', `${column.caption} ${text}`, $summaryItemElement); + $summaryItems.push($summaryItemElement); } $cell.append($summaryItems); } @@ -204,7 +205,7 @@ export class FooterView extends ColumnsView { } protected _renderCellContent($cell, options) { - renderSummaryCell($cell, options); + renderSummaryCell($cell, options, this.setAria.bind(this)); // @ts-expect-error super._renderCellContent.apply(this, arguments); } @@ -911,7 +912,7 @@ const rowsView = (Base: ModuleType) => class SummaryRowsViewExtender e protected _getCellTemplate(options) { if (!options.column.command && !isDefined(options.column.groupIndex) && options.summaryItems && options.summaryItems.length) { - return renderSummaryCell; + return (cell, options) => renderSummaryCell(cell, options, this.setAria.bind(this)); } return super._getCellTemplate(options); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/grid.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/grid.integration.test.ts new file mode 100644 index 000000000000..58f928d66fe3 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/grid.integration.test.ts @@ -0,0 +1,82 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import fx from '@js/common/core/animation/fx'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; +import DataGrid from '@js/ui/data_grid'; +import { DataGridModel } from '@ts/grids/data_grid/__tests__/__mock__/model/data_grid'; + +const SELECTORS = { + gridContainer: '#gridContainer', +}; + +const GRID_CONTAINER_ID = 'gridContainer'; + +const createDataGrid = async ( + options: DataGridProperties = {}, +): Promise<{ + $container: dxElementWrapper; + component: DataGridModel; + instance: DataGrid; +}> => new Promise((resolve) => { + const $container = $('
') + .attr('id', GRID_CONTAINER_ID) + .appendTo(document.body); + + const dataGridOptions: DataGridProperties = { + keyExpr: 'id', + ...options, + }; + + const instance = new DataGrid($container.get(0) as HTMLDivElement, dataGridOptions); + const component = new DataGridModel($container.get(0) as HTMLElement); + + jest.runAllTimers(); + + resolve({ + $container, + component, + instance, + }); +}); + +const beforeTest = (): void => { + fx.off = true; + jest.useFakeTimers(); +}; + +const afterTest = (): void => { + const $container = $(SELECTORS.gridContainer); + const dataGrid = ($container as any).dxDataGrid('instance') as DataGrid; + + dataGrid.dispose(); + $container.remove(); + jest.clearAllMocks(); + jest.useRealTimers(); + fx.off = false; +}; + +describe('Grid', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('when column caption has a newline character', () => { + it('should exclude the newline character from the header filter\'s aria-label', async () => { + const { component } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + ], + columns: ['id', { dataField: 'name', caption: 'Test\nName' }, 'value'], + showBorders: true, + headerFilter: { + visible: true, + }, + }); + + expect(component.getHeaderCellFilter(1).attr('aria-label')).not.toMatch(/\n/); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/adaptivity/m_adaptivity.ts b/packages/devextreme/js/__internal/grids/grid_core/adaptivity/m_adaptivity.ts index f89d17b0d022..ec350d4f3ccf 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/adaptivity/m_adaptivity.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/adaptivity/m_adaptivity.ts @@ -849,7 +849,10 @@ export class AdaptiveColumnsController extends modules.ViewController { public setCommandAdaptiveAriaLabel($row, labelName) { const $adaptiveCommand = $row.find('.dx-command-adaptive'); - $adaptiveCommand.attr('aria-label', messageLocalization.format(labelName)); + + if ($adaptiveCommand.length) { + this.setAria('label', messageLocalization.format(labelName), $adaptiveCommand); + } } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts index 80b32d88c50e..9b559e4fd453 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts @@ -2302,7 +2302,10 @@ class EditingControllerImpl extends modules.ViewController { $container.addClass(COMMAND_EDIT_WITH_ICONS_CLASS); const localizationName = this.getButtonLocalizationNames()[button.name]; - localizationName && $button.attr('aria-label', messageLocalization.format(localizationName)); + + if (localizationName) { + this.setAria('label', messageLocalization.format(localizationName), $button); + } } else { $button.text(button.text); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts index 05a530e0f618..f6543b4faf9e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts @@ -135,7 +135,6 @@ const editingControllerExtender = (Base: ModuleType) => class } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars protected _updateEditRowCore(row, skipCurrentRow, isCustomSetCellValue) { const editForm = this._editForm; @@ -219,7 +218,7 @@ const editingControllerExtender = (Base: ModuleType) => class formTemplate(this._$popupContent, templateOptions, { isPopupForm: true }); this._rowsView.renderDelayedTemplates(); - $(container).parent().attr('aria-label', this.localize('dxDataGrid-ariaEditForm')); + this.setAria('label', this.localize('dxDataGrid-ariaEditForm'), $(container).parent()); }; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/error_handling/m_error_handling.ts b/packages/devextreme/js/__internal/grids/grid_core/error_handling/m_error_handling.ts index c81444d4512a..0445b1edb0e2 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/error_handling/m_error_handling.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/error_handling/m_error_handling.ts @@ -82,11 +82,12 @@ export class ErrorHandlingController extends modules.ViewController { private _renderErrorMessage(error) { const message = error.url ? error.message.replace(error.url, '') : error.message || error; const $message = $('
') - .attr('role', 'alert') - .attr('aria-roledescription', messageLocalization.format('dxDataGrid-ariaError')) .addClass(ERROR_MESSAGE_CLASS) .text(message); + this.setAria('role', 'alert', $message); + this.setAria('roledescription', messageLocalization.format('dxDataGrid-ariaError'), $message); + if (error.url) { $('').attr('href', error.url).text(error.url).appendTo($message); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts index e2638d009a88..102dc935629f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts @@ -542,9 +542,9 @@ export const headerFilterMixin = >(Base: T) => class H const indicatorLabel = (messageLocalization.format as any)('dxDataGrid-headerFilterIndicatorLabel', column.caption); - $headerFilterIndicator.attr('aria-label', indicatorLabel); - $headerFilterIndicator.attr('aria-haspopup', 'dialog'); - $headerFilterIndicator.attr('role', 'button'); + this.setAria('label', indicatorLabel, $headerFilterIndicator); + this.setAria('haspopup', 'dialog', $headerFilterIndicator); + this.setAria('role', 'button', $headerFilterIndicator); } return $headerFilterIndicator; diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts index 3aff0ebc0577..725add2ad56e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts @@ -93,7 +93,9 @@ export class HeaderPanel extends ColumnsView { const $headerPanel = this.element(); $headerPanel.addClass(this.addWidgetPrefix(HEADER_PANEL_CLASS)); const label = messageLocalization.format(this.component.NAME + TOOLBAR_ARIA_LABEL); - const $toolbar = $('
').attr('aria-label', label).appendTo($headerPanel); + const $toolbar = $('
').appendTo($headerPanel); + + this.setAria('label', label, $toolbar); this._toolbar = this._createComponent($toolbar, Toolbar, this._toolbarOptions); } else { this._toolbar.option(this._toolbarOptions!); diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts b/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts index 98b7a1b77786..f606500ebbb1 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ /* eslint-disable max-classes-per-file */ -/* eslint-disable @typescript-eslint/method-signature-style */ + import messageLocalization from '@js/common/core/localization/message'; import type { Component } from '@js/core/component'; import type { dxElementWrapper } from '@js/core/renderer'; @@ -145,14 +145,15 @@ export class ModuleItem { return this._actions[actionName]; } - public setAria(name, value, $target) { + public setAria(name: string, value: any, $target: dxElementWrapper) { const target = $target.get(0); const prefix = name !== 'role' && name !== 'id' ? 'aria-' : ''; + const normalizedValue = String(value).replace(/\s+/g, ' ').trim(); - if (target.setAttribute) { - target.setAttribute(prefix + name, value); + if (target?.setAttribute) { + target.setAttribute(prefix + name, normalizedValue); } else { - $target.attr(prefix + name, value); + $target.attr(prefix + name, normalizedValue); } } @@ -485,9 +486,8 @@ export function processModules( rootViewTypes, ); - // eslint-disable-next-line no-param-reassign componentInstance._controllers = createModuleItems(controllerTypes); - // eslint-disable-next-line no-param-reassign + componentInstance._views = createModuleItems(viewTypes); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts b/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts index 7a906d37c537..aa6982bf02cd 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/master_detail/m_master_detail.ts @@ -396,7 +396,7 @@ const rowsView = (Base: ModuleType) => class RowsViewMasterDetailExten const isEditForm = row.isEditing; if (!isEditForm) { - $detailCell.attr('aria-roledescription', messageLocalization.format('dxDataGrid-masterDetail')); + this.setAria('roledescription', messageLocalization.format('dxDataGrid-masterDetail'), $detailCell); } return $detailCell; diff --git a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts index 6c8dc69fdaeb..5c81455478b3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts @@ -1390,8 +1390,9 @@ export const validatingEditorFactoryExtender = (Base: ModuleType) if (shouldSetValidationAriaAttributes) { const $focusElement = this._getCurrentFocusElement($focus); - $focusElement.attr('aria-labelledby', inputDescriptionValues.join(' ')); - $focusElement.attr('aria-invalid', true); + + this.setAria('labelledby', inputDescriptionValues.join(' '), $focusElement); + this.setAria('invalid', true, $focusElement); } } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/summaryModule.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/summaryModule.tests.js index 43f021b42c20..0302d464cc70 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/summaryModule.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/summaryModule.tests.js @@ -8,6 +8,7 @@ import { addShadowDomStyles } from 'core/utils/shadow_dom'; import * as summaryModule from '__internal/grids/data_grid/summary/m_summary'; import gridCoreUtils from '__internal/grids/grid_core/m_utils'; +import { noop } from 'core/utils/common'; QUnit.testStart(function() { const markup = @@ -448,7 +449,7 @@ QUnit.module('Summary footer', { }], column: { alignment: 'left' }, summaryTexts: summaryTexts - }); + }, noop); // assert assert.equal($cellElements[0].find('.dx-datagrid-summary-item').text(), 119, 'column is not command'); @@ -462,7 +463,7 @@ QUnit.module('Summary footer', { }], column: { command: 'expand', alignment: 'left' }, summaryTexts: summaryTexts - }); + }, noop); // assert assert.equal($cellElements[1].html(), '', 'command column');