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..8dbc6081f7e6 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,25 +37,25 @@ 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; const $summaryItems: any = []; if (!column.command && summaryItems) { - for (let i = 0; i < summaryItems.length; i++) { - const summaryItem = summaryItems[i]; + for (const summaryItem of summaryItems) { 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 +204,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 +911,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__/__mock__/model/grid_core.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts index 48c30555935a..4c61c75c9b5c 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts @@ -11,6 +11,8 @@ const SELECTORS = { headerRowClass: 'dx-header-row', dataRowClass: 'dx-data-row', groupRowClass: 'dx-group-row', + headerCellIndicators: 'dx-column-indicators', + headerCellFilter: 'dx-header-filter', }; export abstract class GridCoreModel { @@ -60,5 +62,12 @@ export abstract class GridCoreModel { return new ColumnChooserModel(this.root); } + public getHeaderCellFilter(columnIndex: number): dxElementWrapper { + const $headerCell = $(this.getHeaderCells()[columnIndex]); + const headerFilterSelector = `.${SELECTORS.headerCellIndicators} > .${SELECTORS.headerCellFilter}`; + + return $headerCell.find(headerFilterSelector); + } + public abstract getInstance(): TInstance; } 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..6320bacd0596 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/grid.integration.test.ts @@ -0,0 +1,83 @@ +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')) + .toBe('Show filter options for column \'Test Name\''); + }); + }); +}); 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 2cbfaef7bb73..c51abcaf1329 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 @@ -836,7 +836,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 d73f533a203c..2abd7752a7b8 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 @@ -78,11 +78,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..5fa9f6f4a6dc 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'; @@ -9,7 +9,7 @@ import Callbacks from '@js/core/utils/callbacks'; // @ts-expect-error import { grep } from '@js/core/utils/common'; import { each } from '@js/core/utils/iterator'; -import { isFunction } from '@js/core/utils/type'; +import { isDefined, isFunction } from '@js/core/utils/type'; import { hasWindow } from '@js/core/utils/window'; import errors from '@js/ui/widget/ui.errors'; @@ -145,14 +145,23 @@ export class ModuleItem { return this._actions[actionName]; } - public setAria(name, value, $target) { + public setAria( + name: string, + value: string | number | boolean | undefined, + $target: dxElementWrapper, + ) { + if (!isDefined(value)) { + return; + } + 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 +494,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/selection/m_selection.ts b/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts index da67a32f4c54..503d34b73238 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts @@ -899,7 +899,7 @@ export const rowsViewSelectionExtenderMixin = (Base: ModuleType) => cl $row .toggleClass(ROW_SELECTION_CLASS, isSelected === undefined ? false : isSelected) .find(`.${SELECT_CHECKBOX_CLASS}`).dxCheckBox('option', 'value', isSelected); - that.setAria('selected', isSelected, $row); + that.setAria('selected', String(isSelected), $row); } } }); @@ -949,7 +949,7 @@ export const rowsViewSelectionExtenderMixin = (Base: ModuleType) => cl const selectionMode = this.option(SELECTION_MODE); if (selectionMode !== 'none') { - this.setAria('selected', isSelected, $row); + this.setAria('selected', String(isSelected), $row); } } 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/gridView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js index e6d65c183617..65725d04292f 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/gridView.tests.js @@ -532,9 +532,9 @@ QUnit.module('Grid view', { ], totalItem: { summaryCells: [ - { summaryType: 'count', value: 100 }, - { summaryType: 'min', value: 0 }, - { summaryType: 'max', value: 120001 } + [{ summaryType: 'count', value: 100 }], + [{ summaryType: 'min', value: 0 }], + [{ summaryType: 'max', value: 120001 }] ] } }) @@ -592,7 +592,7 @@ QUnit.module('Grid view', { ], totalItem: { summaryCells: [ - { summaryType: 'count', value: 100 } + [{ summaryType: 'count', value: 100 }] ] } }) @@ -1626,9 +1626,9 @@ QUnit.module('Synchronize columns', { items: [{ values: [''] }], totalItem: { summaryCells: [ - { summaryType: 'count', value: 100 }, - { summaryType: 'min', value: 0 }, - { summaryType: 'max', value: 120001 } + [{ summaryType: 'count', value: 100 }], + [{ summaryType: 'min', value: 0 }], + [{ summaryType: 'max', value: 120001 }] ] } }) @@ -1663,9 +1663,9 @@ QUnit.module('Synchronize columns', { items: [{ values: [''] }], totalItem: { summaryCells: [ - { summaryType: 'count', value: 100 }, - { summaryType: 'min', value: 0 }, - { summaryType: 'max', value: 120001 } + [{ summaryType: 'count', value: 100 }], + [{ summaryType: 'min', value: 0 }], + [{ summaryType: 'max', value: 120001 }] ] } }) 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');