From d61228d0c4f098fa0fa7d0568b2207012bc61c10 Mon Sep 17 00:00:00 2001 From: Maksim Zakharov <251575087+bit-byte0@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:19:24 +0400 Subject: [PATCH 1/3] fix(pivotGrid): make first data cell focusable for scrollable region keyboard access --- .../pivot_grid/data_area/m_data_area.test.ts | 74 +++++++++++++++++++ .../grids/pivot_grid/data_area/m_data_area.ts | 15 ++++ .../pivotGrid.markup.tests.js | 2 +- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.test.ts diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.test.ts b/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.test.ts new file mode 100644 index 000000000000..d976070ccba4 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.test.ts @@ -0,0 +1,74 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, +} from '@jest/globals'; +import $ from '@js/core/renderer'; + +import { DataArea } from './m_data_area'; + +const createComponent = (): unknown => ({ + option: (optionName?: string) => { + if (optionName === undefined) { + return {}; + } + + return { + rtlEnabled: false, + encodeHtml: false, + }[optionName]; + }, + _eventsStrategy: { hasEvent: () => false }, + _defaultActionArgs: () => ({}), +}); + +describe('DataArea', () => { + let container: HTMLElement = document.createElement('div'); + + const cellsData = [ + [{ text: '1' }, { text: '2' }], + [{ text: '3' }, { text: '4' }], + ]; + + const renderDataArea = (data: unknown[]): DataArea => { + const area = new DataArea(createComponent()); + + area.render($(container), data); + + return area; + }; + + const getCellsTabIndexes = (): (string | null)[] => Array.from(container.querySelectorAll('td')) + .map((cell) => cell.getAttribute('tabindex')); + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); + + describe('render', () => { + it('should make only the first data cell focusable', () => { + renderDataArea(cellsData); + + expect(getCellsTabIndexes()).toEqual(['0', null, null, null]); + }); + + it('should keep the first data cell focusable after re-render', () => { + const area = renderDataArea(cellsData); + + area.render($(container), cellsData); + + expect(getCellsTabIndexes()).toEqual(['0', null, null, null]); + }); + + it('should not fail when there is no data', () => { + expect(() => renderDataArea([])).not.toThrow(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.ts b/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.ts index 785b4c3ca487..5020e7bb808f 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.ts @@ -1,5 +1,6 @@ /* eslint-disable class-methods-use-this */ import $ from '@js/core/renderer'; +import { setTabIndex } from '@js/ui/shared/accessibility'; import supportUtils from '@ts/core/utils/m_support'; import { AreaItem } from '../area_item/m_area_item'; @@ -16,6 +17,20 @@ class DataArea extends AreaItem { return 'data'; } + render(rootElement, data) { + super.render(rootElement, data); + + this._makeFirstCellFocusable(); + } + + _makeFirstCellFocusable() { + const $firstCell = this.tableElement().find('td').first(); + + if ($firstCell.length) { + setTabIndex(this.component, $firstCell); + } + } + _createGroupElement() { return $('
') .addClass(PIVOTGRID_AREA_CLASS) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.markup.tests.js index 42dbaa4708e6..efcec9d9b0b0 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.markup.tests.js @@ -183,7 +183,7 @@ QUnit.module('PivotGrid markup tests', () => { assert.ok($dataCell.length > 0, 'data cell exists'); assert.strictEqual($dataCell.find('.dx-expand-icon-container').length, 0, 'no expand control in a non-expandable cell'); assert.strictEqual($dataCell.attr('role'), undefined, 'data cell has no role'); - assert.strictEqual($dataCell.attr('tabindex'), undefined, 'data cell is not focusable'); + assert.strictEqual($dataCell.attr('tabindex'), '0', 'first data cell is focusable to provide keyboard access to the scrollable data area'); } finally { clock.restore(); } From ac86aec4cdb8fe0cf8136f0e9d6d0843d156bdb6 Mon Sep 17 00:00:00 2001 From: Maksim Zakharov <251575087+bit-byte0@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:13:24 +0400 Subject: [PATCH 2/3] fix(pivotGrid): avoid full table scan when focusing first data cell --- .../js/__internal/grids/pivot_grid/data_area/m_data_area.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.ts b/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.ts index 5020e7bb808f..9224f99d8f82 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/data_area/m_data_area.ts @@ -24,10 +24,10 @@ class DataArea extends AreaItem { } _makeFirstCellFocusable() { - const $firstCell = this.tableElement().find('td').first(); + const firstCell = this.tableElement().get(0)?.querySelector('td'); - if ($firstCell.length) { - setTabIndex(this.component, $firstCell); + if (firstCell) { + setTabIndex(this.component, $(firstCell)); } } From f792f99419f51f33dc3e578b21af37ceac7a2dcc Mon Sep 17 00:00:00 2001 From: Maksim Zakharov <251575087+bit-byte0@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:13:40 +0400 Subject: [PATCH 3/3] fix(pivotGrid): support no-arg option() call in QUnit area stub component --- .../tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.tests.js index ed9b78f80faf..35a708a7777d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/pivotGrid.tests.js @@ -5707,7 +5707,7 @@ function getStubComponent(options) { options = options || {}; return { option: function() { - return options[arguments[0]]; + return arguments.length === 0 ? options : options[arguments[0]]; }, _defaultActionArgs: function() { return {};