From 50e613cf47e41db16d661fe38974f902e8a2a308 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Mon, 25 May 2026 20:23:20 +0000 Subject: [PATCH] PivotGrid - Add keyboard accessibility to expand icons --- .../expand_icon_accessibility.test.ts | 86 +++++++++++++++++++ .../grids/pivot_grid/area_item/m_area_item.ts | 2 + .../__internal/grids/pivot_grid/m_widget.ts | 11 +++ 3 files changed, 99 insertions(+) create mode 100644 packages/devextreme/js/__internal/grids/pivot_grid/__tests__/expand_icon_accessibility.test.ts diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/__tests__/expand_icon_accessibility.test.ts b/packages/devextreme/js/__internal/grids/pivot_grid/__tests__/expand_icon_accessibility.test.ts new file mode 100644 index 000000000000..935b981e92c7 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/pivot_grid/__tests__/expand_icon_accessibility.test.ts @@ -0,0 +1,86 @@ +import $ from '@js/core/renderer'; + +import { AreaItem } from '../area_item/m_area_item'; + +class TestAreaItem extends AreaItem { + _getAreaName() { + return 'row'; + } +} + +const createMockComponent = () => ({ + option(name: string) { + const options: Record = { + rtlEnabled: false, + encodeHtml: true, + onCellPrepared: null, + }; + return options[name]; + }, + _eventsStrategy: { + hasEvent: () => false, + }, + _defaultActionArgs: () => ({}), +}); + +describe('PivotGrid expand icon accessibility', () => { + let $container: any = null; + + beforeEach(() => { + $container = $('
').appendTo('body'); + }); + + afterEach(() => { + $container.remove(); + }); + + it('should set aria-expanded="true" on td when cell is expanded', () => { + const areaItem = new TestAreaItem(createMockComponent()); + const tableData = [[{ expanded: true, text: 'Category' }]]; + + areaItem.render($container, tableData); + + const $td = $container.find('td'); + expect($td.attr('aria-expanded')).toBe('true'); + }); + + it('should set aria-expanded="false" on td when cell is collapsed', () => { + const areaItem = new TestAreaItem(createMockComponent()); + const tableData = [[{ expanded: false, text: 'Category' }]]; + + areaItem.render($container, tableData); + + const $td = $container.find('td'); + expect($td.attr('aria-expanded')).toBe('false'); + }); + + it('should set tabindex="0" on td when cell has expand icon', () => { + const areaItem = new TestAreaItem(createMockComponent()); + const tableData = [[{ expanded: true, text: 'Category' }]]; + + areaItem.render($container, tableData); + + const $td = $container.find('td'); + expect($td.attr('tabindex')).toBe('0'); + }); + + it('should not set tabindex on td when cell has no expand state', () => { + const areaItem = new TestAreaItem(createMockComponent()); + const tableData = [[{ text: 'Plain cell' }]]; + + areaItem.render($container, tableData); + + const $td = $container.find('td'); + expect($td.attr('tabindex')).toBeUndefined(); + }); + + it('should not set aria-expanded on td when cell has no expand state', () => { + const areaItem = new TestAreaItem(createMockComponent()); + const tableData = [[{ text: 'Plain cell' }]]; + + areaItem.render($container, tableData); + + const $td = $container.find('td'); + expect($td.attr('aria-expanded')).toBeUndefined(); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/area_item/m_area_item.ts b/packages/devextreme/js/__internal/grids/pivot_grid/area_item/m_area_item.ts index dbc68392969d..340579bfbfd9 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/area_item/m_area_item.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/area_item/m_area_item.ts @@ -198,6 +198,8 @@ abstract class AreaItem { span.classList.add(PIVOTGRID_EXPAND_CLASS); div.appendChild(span); td.appendChild(div); + td.setAttribute('aria-expanded', String(cell.expanded)); + td.setAttribute('tabindex', '0'); } cellText = this._getCellText(cell, encodeHtml); diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts index c1b08371354e..a47c0faf48f5 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -886,6 +886,16 @@ class PivotGrid extends Widget { }); } + _handleCellKeyDown(e) { + if (e.key === 'Enter' || e.key === ' ') { + const args = this._createEventArgs(e.currentTarget, e); + if (args.cell && isDefined(args.cell.expanded)) { + e.preventDefault(); + this._handleCellClick({ currentTarget: e.currentTarget, preventDefault: noop }); + } + } + } + _getNoDataText() { return this.option('texts.noData'); } @@ -1077,6 +1087,7 @@ class PivotGrid extends Widget { .toggleClass('dx-word-wrap', !!that.option('wordWrapEnabled')); eventsEngine.on($table, addNamespace(clickEventName, 'dxPivotGrid'), 'td', that._handleCellClick.bind(that)); + eventsEngine.on($table, addNamespace('keydown', 'dxPivotGrid'), 'td', that._handleCellKeyDown.bind(that)); return $table; }