From 656281a371f62268a95dba5f7f34b0d52387fa25 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 29 May 2026 10:07:53 -0300 Subject: [PATCH 1/9] PivotGrid A11y and KBN - The expand icons are not accessible via keyboard (KBN) --- .../grids/pivot_grid/area_item/m_area_item.ts | 2 + .../__internal/grids/pivot_grid/m_widget.ts | 11 +++ .../pivotGrid.markup.tests.js | 65 ++++++++++++++ .../pivotGrid.tests.js | 89 +++++++++++++++++++ 4 files changed, 167 insertions(+) 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 f4ab393f4a55..67b29cfeb96b 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -883,6 +883,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'); } @@ -1074,6 +1084,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; } 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 bd9ec652f5ad..9952e729a932 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 @@ -85,5 +85,70 @@ QUnit.module('PivotGrid markup tests', () => { clock.restore(); }); + const createExpandableDataSource = () => ({ + fields: [ + { dataField: 'region', area: 'row' }, + { dataField: 'city', area: 'row' }, + { dataField: 'year', area: 'column', expanded: true }, + { dataField: 'quarter', area: 'column' }, + { dataField: 'amount', area: 'data', summaryType: 'sum', dataType: 'number' } + ], + store: [ + { region: 'N', city: 'B', year: 2020, quarter: 'Q1', amount: 100 }, + { region: 'N', city: 'NY', year: 2020, quarter: 'Q2', amount: 200 }, + { region: 'S', city: 'M', year: 2021, quarter: 'Q1', amount: 300 } + ] + }); + + QUnit.test('Expandable td has aria-expanded reflecting expanded state', function(assert) { + const clock = sinon.useFakeTimers(); + try { + const pivotGrid = createPivotGrid({ dataSource: createExpandableDataSource() }); + clock.tick(10); + + const $expandedTd = pivotGrid.$element().find('.dx-pivotgrid-expanded').first().closest('td'); + const $collapsedTd = pivotGrid.$element().find('.dx-pivotgrid-collapsed').first().closest('td'); + + assert.ok($expandedTd.length > 0, 'expanded td present'); + assert.ok($collapsedTd.length > 0, 'collapsed td present'); + assert.strictEqual($expandedTd.attr('aria-expanded'), 'true', 'expanded td has aria-expanded="true"'); + assert.strictEqual($collapsedTd.attr('aria-expanded'), 'false', 'collapsed td has aria-expanded="false"'); + } finally { + clock.restore(); + } + }); + + QUnit.test('Expandable td has tabindex="0"', function(assert) { + const clock = sinon.useFakeTimers(); + try { + const pivotGrid = createPivotGrid({ dataSource: createExpandableDataSource() }); + clock.tick(10); + + const $expandedTd = pivotGrid.$element().find('.dx-pivotgrid-expanded').first().closest('td'); + const $collapsedTd = pivotGrid.$element().find('.dx-pivotgrid-collapsed').first().closest('td'); + + assert.strictEqual($expandedTd.attr('tabindex'), '0', 'expanded td is focusable'); + assert.strictEqual($collapsedTd.attr('tabindex'), '0', 'collapsed td is focusable'); + } finally { + clock.restore(); + } + }); + + QUnit.test('Non-expandable td has neither aria-expanded nor tabindex', function(assert) { + const clock = sinon.useFakeTimers(); + try { + const pivotGrid = createPivotGrid({ dataSource: createExpandableDataSource() }); + clock.tick(10); + + const $nonExpandableTd = pivotGrid.$element().find('td:not([aria-expanded])').first(); + + assert.ok($nonExpandableTd.length > 0, 'non-expandable td exists'); + assert.strictEqual($nonExpandableTd.attr('aria-expanded'), undefined, 'no aria-expanded attribute'); + assert.strictEqual($nonExpandableTd.attr('tabindex'), undefined, 'no tabindex attribute'); + } finally { + clock.restore(); + } + }); + }); 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 d3610a69ba07..ad9d101c0253 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 @@ -433,6 +433,95 @@ QUnit.module('dxPivotGrid', { assert.strictEqual(expandValueChangingArgs, undefined); }); + QUnit.test('expand column item by Enter keydown', function(assert) { + let expandValueChangingArgs; + const pivotGrid = createPivotGrid({ + dataSource: this.dataSource, + onExpandValueChanging: function(args) { + expandValueChangingArgs = $.extend({}, args); + } + }); + assert.ok(pivotGrid); + + const $collapsedTd = $('#pivotGrid').find('.dx-pivotgrid-collapsed').closest('td'); + assert.strictEqual($collapsedTd.length, 1); + + $collapsedTd.trigger($.Event('keydown', { key: 'Enter' })); + + this.clock.tick(10); + + assert.deepEqual(expandValueChangingArgs, { + area: 'column', + path: ['2012'], + expanded: true, + needExpandData: true + }); + }); + + QUnit.test('collapse column item by Space keydown', function(assert) { + let expandValueChangingArgs; + const pivotGrid = createPivotGrid({ + dataSource: this.dataSource, + onExpandValueChanging: function(args) { + expandValueChangingArgs = $.extend({}, args); + } + }); + assert.ok(pivotGrid); + + const $expandedTd = $('#pivotGrid').find('.dx-pivotgrid-expanded').closest('td'); + assert.strictEqual($expandedTd.length, 1); + + $expandedTd.trigger($.Event('keydown', { key: ' ' })); + + this.clock.tick(10); + + assert.deepEqual(expandValueChangingArgs, { + area: 'column', + path: ['2010'], + expanded: false + }); + }); + + QUnit.test('keydown with keys other than Enter and Space does not toggle expansion', function(assert) { + let expandValueChangingArgs; + const pivotGrid = createPivotGrid({ + dataSource: this.dataSource, + onExpandValueChanging: function(args) { + expandValueChangingArgs = $.extend({}, args); + } + }); + assert.ok(pivotGrid); + + const $collapsedTd = $('#pivotGrid').find('.dx-pivotgrid-collapsed').closest('td'); + assert.strictEqual($collapsedTd.length, 1); + + $collapsedTd.trigger($.Event('keydown', { key: 'Tab' })); + + this.clock.tick(10); + + assert.strictEqual(expandValueChangingArgs, undefined); + }); + + QUnit.test('keydown on a non-expandable td does not toggle expansion', function(assert) { + let expandValueChangingArgs; + const pivotGrid = createPivotGrid({ + dataSource: this.dataSource, + onExpandValueChanging: function(args) { + expandValueChangingArgs = $.extend({}, args); + } + }); + assert.ok(pivotGrid); + + const $nonExpandableTd = $('#pivotGrid').find('td:not([aria-expanded])').first(); + assert.ok($nonExpandableTd.length > 0); + + $nonExpandableTd.trigger($.Event('keydown', { key: 'Enter' })); + + this.clock.tick(10); + + assert.strictEqual(expandValueChangingArgs, undefined); + }); + QUnit.test('T248253. DataSource changed', function(assert) { let expandValueChangingArgs; const pivotGrid = createPivotGrid($.extend(this.testOptions, { From 6e8588ef907ae4d4cb27da818c1a0e101bf5cdec Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 29 May 2026 10:45:32 -0300 Subject: [PATCH 2/9] PivotGrid - Skip markup a11y tests on serverSide --- .../pivotGrid.markup.tests.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 9952e729a932..da52cb0a83f3 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 @@ -101,6 +101,10 @@ QUnit.module('PivotGrid markup tests', () => { }); QUnit.test('Expandable td has aria-expanded reflecting expanded state', function(assert) { + if(!windowUtils.hasWindow()) { + assert.ok(true, 'skipped on serverSide'); + return; + } const clock = sinon.useFakeTimers(); try { const pivotGrid = createPivotGrid({ dataSource: createExpandableDataSource() }); @@ -119,6 +123,10 @@ QUnit.module('PivotGrid markup tests', () => { }); QUnit.test('Expandable td has tabindex="0"', function(assert) { + if(!windowUtils.hasWindow()) { + assert.ok(true, 'skipped on serverSide'); + return; + } const clock = sinon.useFakeTimers(); try { const pivotGrid = createPivotGrid({ dataSource: createExpandableDataSource() }); @@ -135,6 +143,10 @@ QUnit.module('PivotGrid markup tests', () => { }); QUnit.test('Non-expandable td has neither aria-expanded nor tabindex', function(assert) { + if(!windowUtils.hasWindow()) { + assert.ok(true, 'skipped on serverSide'); + return; + } const clock = sinon.useFakeTimers(); try { const pivotGrid = createPivotGrid({ dataSource: createExpandableDataSource() }); From b90415ba32b9ee9240f6a7ceee86e442ee54fbdc Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 29 May 2026 12:15:50 -0300 Subject: [PATCH 3/9] PivotGrid - Refine keyboard a11y per review feedback --- .../scss/widgets/base/pivotGrid/_common.scss | 5 ++++ .../__internal/grids/pivot_grid/m_widget.ts | 2 +- .../pivotGrid.tests.js | 25 ++++++++++++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/pivotGrid/_common.scss b/packages/devextreme-scss/scss/widgets/base/pivotGrid/_common.scss index 87826aab0aca..0faf4d470dca 100644 --- a/packages/devextreme-scss/scss/widgets/base/pivotGrid/_common.scss +++ b/packages/devextreme-scss/scss/widgets/base/pivotGrid/_common.scss @@ -75,6 +75,11 @@ box-sizing: content-box; } + td[aria-expanded]:focus-visible { + outline: 2px solid currentColor; + outline-offset: -2px; + } + .dx-area-description-cell { position: relative; background-clip: padding-box; // T379462 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 67b29cfeb96b..14106e642a4b 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -888,7 +888,7 @@ class PivotGrid extends Widget { const args = this._createEventArgs(e.currentTarget, e); if (args.cell && isDefined(args.cell.expanded)) { e.preventDefault(); - this._handleCellClick({ currentTarget: e.currentTarget, preventDefault: noop }); + this._handleCellClick(e); } } } 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 ad9d101c0253..33914e15c297 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 @@ -512,7 +512,7 @@ QUnit.module('dxPivotGrid', { }); assert.ok(pivotGrid); - const $nonExpandableTd = $('#pivotGrid').find('td:not([aria-expanded])').first(); + const $nonExpandableTd = $('#pivotGrid').find('.dx-area-row-cell td').first(); assert.ok($nonExpandableTd.length > 0); $nonExpandableTd.trigger($.Event('keydown', { key: 'Enter' })); @@ -522,6 +522,29 @@ QUnit.module('dxPivotGrid', { assert.strictEqual(expandValueChangingArgs, undefined); }); + QUnit.test('onCellClick cancel prevents keyboard expansion', function(assert) { + let expandValueChangingArgs; + const pivotGrid = createPivotGrid({ + dataSource: this.dataSource, + onExpandValueChanging: function(args) { + expandValueChangingArgs = $.extend({}, args); + }, + onCellClick: function(args) { + args.cancel = true; + } + }); + assert.ok(pivotGrid); + + const $collapsedTd = $('#pivotGrid').find('.dx-pivotgrid-collapsed').closest('td'); + assert.strictEqual($collapsedTd.length, 1); + + $collapsedTd.trigger($.Event('keydown', { key: 'Enter' })); + + this.clock.tick(10); + + assert.strictEqual(expandValueChangingArgs, undefined); + }); + QUnit.test('T248253. DataSource changed', function(assert) { let expandValueChangingArgs; const pivotGrid = createPivotGrid($.extend(this.testOptions, { From 0764a9d20eae394da8f2f8e73c12c5ae773fd840 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 29 May 2026 15:22:47 -0300 Subject: [PATCH 4/9] PivotGrid - Add role="button" to expandable cells --- .../grids/pivot_grid/area_item/m_area_item.ts | 1 + .../pivotGrid.markup.tests.js | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) 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 340579bfbfd9..eacf3c9f1fee 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,7 @@ abstract class AreaItem { span.classList.add(PIVOTGRID_EXPAND_CLASS); div.appendChild(span); td.appendChild(div); + td.setAttribute('role', 'button'); td.setAttribute('aria-expanded', String(cell.expanded)); td.setAttribute('tabindex', '0'); } 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 da52cb0a83f3..99ea944101cb 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 @@ -142,7 +142,27 @@ QUnit.module('PivotGrid markup tests', () => { } }); - QUnit.test('Non-expandable td has neither aria-expanded nor tabindex', function(assert) { + QUnit.test('Expandable td has role="button"', function(assert) { + if(!windowUtils.hasWindow()) { + assert.ok(true, 'skipped on serverSide'); + return; + } + const clock = sinon.useFakeTimers(); + try { + const pivotGrid = createPivotGrid({ dataSource: createExpandableDataSource() }); + clock.tick(10); + + const $expandedTd = pivotGrid.$element().find('.dx-pivotgrid-expanded').first().closest('td'); + const $collapsedTd = pivotGrid.$element().find('.dx-pivotgrid-collapsed').first().closest('td'); + + assert.strictEqual($expandedTd.attr('role'), 'button', 'expanded td has role="button"'); + assert.strictEqual($collapsedTd.attr('role'), 'button', 'collapsed td has role="button"'); + } finally { + clock.restore(); + } + }); + + QUnit.test('Non-expandable td has no role, aria-expanded, or tabindex', function(assert) { if(!windowUtils.hasWindow()) { assert.ok(true, 'skipped on serverSide'); return; @@ -155,6 +175,7 @@ QUnit.module('PivotGrid markup tests', () => { const $nonExpandableTd = pivotGrid.$element().find('td:not([aria-expanded])').first(); assert.ok($nonExpandableTd.length > 0, 'non-expandable td exists'); + assert.strictEqual($nonExpandableTd.attr('role'), undefined, 'no role attribute'); assert.strictEqual($nonExpandableTd.attr('aria-expanded'), undefined, 'no aria-expanded attribute'); assert.strictEqual($nonExpandableTd.attr('tabindex'), undefined, 'no tabindex attribute'); } finally { From 643501d6a4b152083c9844e04917c280b271c1ac Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 29 May 2026 15:43:03 -0300 Subject: [PATCH 5/9] PivotGrid - Ignore key auto-repeat in keyboard expand handler --- .../__internal/grids/pivot_grid/m_widget.ts | 3 +++ .../pivotGrid.tests.js | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) 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 14106e642a4b..eb710c031dc9 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -884,6 +884,9 @@ class PivotGrid extends Widget { } _handleCellKeyDown(e) { + if (e.repeat) { + return; + } if (e.key === 'Enter' || e.key === ' ') { const args = this._createEventArgs(e.currentTarget, e); if (args.cell && isDefined(args.cell.expanded)) { 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 33914e15c297..f3d2b7f1a4a3 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 @@ -522,6 +522,26 @@ QUnit.module('dxPivotGrid', { assert.strictEqual(expandValueChangingArgs, undefined); }); + QUnit.test('keydown auto-repeat does not trigger expansion', function(assert) { + let expandValueChangingArgs; + const pivotGrid = createPivotGrid({ + dataSource: this.dataSource, + onExpandValueChanging: function(args) { + expandValueChangingArgs = $.extend({}, args); + } + }); + assert.ok(pivotGrid); + + const $collapsedTd = $('#pivotGrid').find('.dx-pivotgrid-collapsed').closest('td'); + assert.strictEqual($collapsedTd.length, 1); + + $collapsedTd.trigger($.Event('keydown', { key: 'Enter', repeat: true })); + + this.clock.tick(10); + + assert.strictEqual(expandValueChangingArgs, undefined); + }); + QUnit.test('onCellClick cancel prevents keyboard expansion', function(assert) { let expandValueChangingArgs; const pivotGrid = createPivotGrid({ From 74b5f609d1211dba2458f7849f03325809471db6 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 29 May 2026 18:39:08 -0300 Subject: [PATCH 6/9] PivotGrid - Restore focus after keyboard expand via shared a11y helpers --- .../grids/pivot_grid/area_item/m_area_item.ts | 1 + .../__internal/grids/pivot_grid/m_widget.ts | 7 +++++ .../pivotGrid.tests.js | 28 +++++++++++++++++++ 3 files changed, 36 insertions(+) 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 eacf3c9f1fee..8160d13be582 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 @@ -199,6 +199,7 @@ abstract class AreaItem { div.appendChild(span); td.appendChild(div); td.setAttribute('role', 'button'); + td.setAttribute('aria-label', String(cell.text ?? cell.value ?? '')); td.setAttribute('aria-expanded', String(cell.expanded)); td.setAttribute('tabindex', '0'); } 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 eb710c031dc9..c2967c7f6ecd 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -20,6 +20,7 @@ import type { Properties } from '@js/ui/button'; import Button from '@js/ui/button'; import ContextMenu from '@js/ui/context_menu'; import Popup from '@js/ui/popup/ui.popup'; +import { restoreFocus, saveFocusedElementInfo } from '@js/ui/shared/accessibility'; import { current, isFluent } from '@js/ui/themes'; import Widget from '@ts/core/widget/widget'; import gridCoreUtils from '@ts/grids/grid_core/m_utils'; @@ -891,6 +892,12 @@ class PivotGrid extends Widget { const args = this._createEventArgs(e.currentTarget, e); if (args.cell && isDefined(args.cell.expanded)) { e.preventDefault(); + saveFocusedElementInfo(e.currentTarget, this); + const onReady = () => { + this.off('contentReady', onReady); + restoreFocus(this); + }; + this.on('contentReady', onReady); this._handleCellClick(e); } } 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 f3d2b7f1a4a3..d0bf5dc05e8c 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 @@ -542,6 +542,34 @@ QUnit.module('dxPivotGrid', { assert.strictEqual(expandValueChangingArgs, undefined); }); + QUnit.test('keyboard activation preserves focus on the same cell after re-render', function(assert) { + let expandValueChangingArgs; + const pivotGrid = createPivotGrid({ + dataSource: this.dataSource, + onExpandValueChanging: function(args) { + expandValueChangingArgs = $.extend({}, args); + } + }); + assert.ok(pivotGrid); + + const $collapsedTd = $('#pivotGrid').find('.dx-pivotgrid-collapsed').closest('td'); + assert.strictEqual($collapsedTd.length, 1); + const ariaLabelBefore = $collapsedTd.attr('aria-label'); + $collapsedTd.get(0).focus(); + + $collapsedTd.trigger($.Event('keydown', { key: 'Enter' })); + + this.clock.tick(100); + + assert.deepEqual(expandValueChangingArgs, { + area: 'column', + path: ['2012'], + expanded: true, + needExpandData: true + }); + assert.strictEqual($(document.activeElement).attr('aria-label'), ariaLabelBefore, 'focus restored to the same cell after expand'); + }); + QUnit.test('onCellClick cancel prevents keyboard expansion', function(assert) { let expandValueChangingArgs; const pivotGrid = createPivotGrid({ From 37ddaece82519122cfff4854ddce2a0c96cf871f Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Mon, 1 Jun 2026 05:43:49 -0300 Subject: [PATCH 7/9] PivotGrid - Use themed focus color for keyboard outline --- .../scss/widgets/base/pivotGrid/_common.scss | 5 ----- .../scss/widgets/base/pivotGrid/_index.scss | 7 +++++++ .../scss/widgets/fluent/pivotGrid/_index.scss | 1 + .../scss/widgets/generic/pivotGrid/_index.scss | 1 + .../scss/widgets/material/pivotGrid/_index.scss | 1 + 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/pivotGrid/_common.scss b/packages/devextreme-scss/scss/widgets/base/pivotGrid/_common.scss index 0faf4d470dca..87826aab0aca 100644 --- a/packages/devextreme-scss/scss/widgets/base/pivotGrid/_common.scss +++ b/packages/devextreme-scss/scss/widgets/base/pivotGrid/_common.scss @@ -75,11 +75,6 @@ box-sizing: content-box; } - td[aria-expanded]:focus-visible { - outline: 2px solid currentColor; - outline-offset: -2px; - } - .dx-area-description-cell { position: relative; background-clip: padding-box; // T379462 diff --git a/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss index b48a8b8b2bf9..44d85b580a69 100644 --- a/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss @@ -18,6 +18,7 @@ $pivotgrid-grandtotalcolor: null !default; $pivotgrid-accent-color: null !default; $pivotgrid-empty-area-text-padding: null !default; $pivotgrid-button-top-padding: null !default; +$base-focus-color: null !default; @use "../mixins" as *; @use "../icon_fonts" as *; @@ -125,6 +126,12 @@ $pivotgrid-expand-icon-text-offset: 0; } .dx-pivotgrid { + td[aria-expanded]:focus-visible { + outline: 2px solid; + outline-color: $base-focus-color; + outline-offset: -2px; + } + .dx-column-header, .dx-filter-header { .dx-pivotgrid-toolbar { diff --git a/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss index 0bb559809a27..5fcfb96100e7 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss @@ -30,6 +30,7 @@ $pivotgrid-accent-color: $pivotgrid-accent-color, $pivotgrid-empty-area-text-padding: $pivotgrid-empty-area-text-padding, $pivotgrid-button-top-padding: $pivotgrid-button-top-padding, + $base-focus-color: $base-focus-color, ); // adduse diff --git a/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_index.scss index 82fc3767fed5..46c964e274c3 100644 --- a/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_index.scss @@ -29,6 +29,7 @@ $pivotgrid-accent-color: $pivotgrid-accent-color, $pivotgrid-empty-area-text-padding: $pivotgrid-empty-area-text-padding, $pivotgrid-button-top-padding: $pivotgrid-button-top-padding, + $base-focus-color: $base-focus-color, ); // adduse diff --git a/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss index 7d8d65d7f38e..740d97537410 100644 --- a/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss @@ -30,6 +30,7 @@ $pivotgrid-accent-color: $pivotgrid-accent-color, $pivotgrid-empty-area-text-padding: $pivotgrid-empty-area-text-padding, $pivotgrid-button-top-padding: $pivotgrid-button-top-padding, + $base-focus-color: $base-focus-color, ); // adduse From 9eb9368c1f18a8fbdca55a0ad3c51973eaf2b9a4 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 4 Jun 2026 09:40:53 -0300 Subject: [PATCH 8/9] PivotGrid - Address Copilot review on dangling subscription and aria-label --- .../grids/pivot_grid/area_item/m_area_item.ts | 2 +- .../__internal/grids/pivot_grid/m_widget.ts | 33 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) 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 8160d13be582..cd51aa9fd4c6 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 @@ -199,7 +199,7 @@ abstract class AreaItem { div.appendChild(span); td.appendChild(div); td.setAttribute('role', 'button'); - td.setAttribute('aria-label', String(cell.text ?? cell.value ?? '')); + td.setAttribute('aria-label', String(cell.value ?? cell.text ?? '')); td.setAttribute('aria-expanded', String(cell.expanded)); td.setAttribute('tabindex', '0'); } 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 c2967c7f6ecd..6fbbc97708aa 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts @@ -888,19 +888,28 @@ class PivotGrid extends Widget { if (e.repeat) { return; } - if (e.key === 'Enter' || e.key === ' ') { - const args = this._createEventArgs(e.currentTarget, e); - if (args.cell && isDefined(args.cell.expanded)) { - e.preventDefault(); - saveFocusedElementInfo(e.currentTarget, this); - const onReady = () => { - this.off('contentReady', onReady); - restoreFocus(this); - }; - this.on('contentReady', onReady); - this._handleCellClick(e); - } + if (e.key !== 'Enter' && e.key !== ' ') { + return; + } + const args = this._createEventArgs(e.currentTarget, e); + const { cell } = args; + if (!cell || !isDefined(cell.expanded)) { + return; } + e.preventDefault(); + this._trigger('onCellClick', args); + if (args.cancel) { + return; + } + saveFocusedElementInfo(e.currentTarget, this); + const onReady = () => { + this.off('contentReady', onReady); + restoreFocus(this); + }; + this.on('contentReady', onReady); + setTimeout(() => { + this._dataController[cell.expanded ? 'collapseHeaderItem' : 'expandHeaderItem'](args.area, cell.path); + }); } _getNoDataText() { From cf5b1d9233c1a44f388e520f5da88f4da980cda4 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 5 Jun 2026 13:33:44 -0300 Subject: [PATCH 9/9] PivotGrid - Fix focus color, type CellClickEvent with KeyboardEvent, add visual test --- ...dable_cell_focused (fluent.blue.light).png | Bin 0 -> 27831 bytes .../tests/common/pivotGrid/kbn/expandIcon.ts | 62 ++++++++++++++++++ .../scss/widgets/base/pivotGrid/_index.scss | 3 +- .../scss/widgets/fluent/pivotGrid/_index.scss | 1 - .../widgets/generic/pivotGrid/_index.scss | 1 - .../widgets/material/pivotGrid/_index.scss | 1 - packages/devextreme/js/ui/pivot_grid.d.ts | 2 +- packages/devextreme/ts/dx.all.d.ts | 2 +- 8 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/etalons/pivotgrid_kbn_expandable_cell_focused (fluent.blue.light).png create mode 100644 e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/expandIcon.ts diff --git a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/etalons/pivotgrid_kbn_expandable_cell_focused (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/etalons/pivotgrid_kbn_expandable_cell_focused (fluent.blue.light).png new file mode 100644 index 0000000000000000000000000000000000000000..58a511795dcba49f5fb99fd6f01f10010b249cf3 GIT binary patch literal 27831 zcmdS9bzEFsvMwAvfyUi~1otLr1HoNGkl^l4aERbE8VK$hg1fuByEpFc1a9-r%y-V5 z^Uj%b@15VdziDt%v6v(g4lW1d57 zkACc(yIZ5Ecdvh6#xE~OD&5?r*_fU=(NrY#i3K}tdFhr2-Q)#g2oe7FNfV>|%H#gm zQS;A7w{yyu6n+>YL1iJYp~FjxZia8qgC9Y`%jW^Zu<&2;z$ptdy5{dL&0n4{>uj_6 z_E*1Y0ufJTMEKH@*B3v;Mb4PjGY_XV^Dkso)sd1YhtA#hk7Mj( z8#rA#d|*v*9&Fr@rGG%&Rz*WiLY2FM)X1gsE@d?=aMq}*UgZ8Ua!f3A;uc{~wj!G| z!r!?@y;jvq)B#+?h?W?Q^?xd-qW=;p7csYmt5>+{cOa0wtdeISdn>9maG!X5lR7c7 zLoF;~=K3TeJeVerj%7-7@FNJf)>@5`vyn4xscEQih#TG~(11yF{!hh3|1&M{zy!3~ zjA_`EpX3A6LN@xZ6OH%LQ9rmf4C{(IY^<=jW*u@!E*tF8py9NEllz5;3m zjFrdegz}v{DSGM6oU$@-83VN2f~)GH%@lmzVXWL|8jUvpujOoYo~!$qGv?EJ;`Z$( z0ao^sal^W6X7EeX6a(YoJwQHrqK>fN>3l;aq-F%8mYJwP-h~W6;gYCrk?OC4r**2- zYf#YLv+sE$*&`PH9w%;($&@)2S(~c>mq<5pB%(fd;Xw`n=XM|BT*MC++Vm!H-{Y2^P@|}4!5Rp@!@AS z84dk8{VZi?99rDpLl{5`<6zvdNR|P@5!#lKN_&wmhVg~ksR<^YI~fmdu@wEstmDP zbWBx<>lfa#tQ=rXDC=SzlsRSuY!AL~;g|QS9l88Lgl`$rVCBx#3Qx97Crx34dGMjG zaxYqQ9-^kU#$BNL>%##*G0yvtQ7e_A>X$}|bSrPNT}MT824+ddfBlK1a7PSr3=N(A z1J+AY9F9qah1%J_s%PnPl^S){45GpK+!>{V_*Q7ON!mL3N*}ZqS#n-ptJVJ3D`NdK zp$!lhCK;08fuD|+9oUZe$Y&TP4Ydk4nO|zFF}_6$%AmbLqiFIaW6dR0kqo1npR`l+0YWxWM$u4(JQX7U;F2Is3MtmjtS2`+y@Fb!!OXK|kN%=v6c z4Fky9@f1RX6MJho1mhZAz=-?H61bKcCuphT}Q09v$-GVsN?gUuETvDW^QZMGVm4*neBvE%RlBjyaNcdxgvYTWoaPx zAfNuZuhhMLEcvXxo`7L@PWgemhfwR(2Gh^&Yg?qThfhL|VL*(4bTeqA5_k0$hUUcl z5KSr$pN@wsHYKB0iE_?r;!3nt|AKSC)fOmZl66BPn%F*$caYc58fLCd%} zzWau(DY68ic|e)T*9di|l0SZs1f?QcLoLMGJ?h)l?DDbU3(?T_D(2~aw2!BmrGX3n z&Rl4ZEH*2-r5cn;<)z7*cv~dxzIe7T0yzD1Q1>=TqFq7;trO5m#YYY?&#n@pD0MV4 zo8x=RY?9~PWi;hSE?*IjTDY<$@Zy=$mKEa6olznjmNl((y$B%ix6&&7L0SnR4prvK zF)3z_>&>-RnWJ>-;_SZO@%Lgr(Fr#sBpG0vwOijK#M^etW&-y;fSfe#7f2BiL%%xT zBD(I0kAFP<_Fu_K;46$d)rdVP6~)4&tT(tm%Qbue$|=mxTKBvVX~z3YEHxoGsByRifX{hVV!MoeQtOy%57FS@y5H4%`SM>ojW*7%?vM z0adb5WGC^@d8)&J{EGAiyLEyTaD0boG{Bzr1M1ixlv=vk=>pZ)4Fs(iS@t&Is&Zd7 z=t=L$CEQh+m;&h!`bfHNEe=L|hzXhvVbSekHsn46|CtIiBbVDEf3EBM|Cyw{HU5Fj zYFYv9B44GuP!uRSQ8=T!7`^&|G$d>QTo*afQpK5B<8izY$(X~)>cpF-7tzw}2?H>Q z5%`)3nlgl`$W7k!&Fz{pS?+WPW4lBuKnLg9j$`5Sc!~3P1xWVW2Hl8AX(n1lw3qx| zd96;oAtv@rFLl!1<@I&qDr)QkV&}Sui5#IuDu|Ff&^IB6S8_r>5^vwiraH@x+9s%D z6fLbqN2XjYI&TTNhLxk~GG;*{0O@8QFm+)mCbVW%0XzACiYn(yUp|kD@|rLKJO9Dr zw7`=SC|XN{M=xJFC(3D``u!NzPta-1pomMziCN1ZUC29ROwQpp(&+QB zX@rL%2i^!s;Z2y6O-LtKn$GNFas+Q~^uZVe;?wcVmUX^=jwOJy;lb!h5Y8(3g^~iH ztbgq?#t$Z}8BngY>G#o#odiv5e$iz_*tBCq#6JMg0<&K5t3q@zV`}L*AyWmIo6|Up zN(39rR*h%7?k0mKCCSXaE-1@=zfdk%5ePDZ4S@6Aoc(DZwBl>b9=64v(kX4_(nLlP zt)Y-gg5l!!wY&mH7Gwe&F+Gg2KM0Z@5`}pbM&FDnQw`cjRqms77&1yUrg(glbq?X&6N0`bE)A6;<(OQ+~ALzMJDzEp43djVpwg|5Mh7%fvfBfat3nNpx#omBB z-$qz`PoU-$@G!+&7>=y@+Pcl}FTkfGYDp80tXdfMV2@+yj8)_&VUg+Q z*Zm^IHI}e+rZzl{r6Q(^O}91ZRpCzvJ+3%o2OZ~wQbtX`=kZcDQfFRVU0Hk=B*Q*v zU(|yloa;ZmKfl%W4`UZ~`@7d8vpFXnJK1i+^mN95)G*?;%i^&J=y37fQVz)Z?#ty# zLO81I4|(N}`ypQ6kNpe8(|?)axXCq|3?ofgwL#;7vF z8fN!N6YR<+U2Iz!U`;t_Ou7<6m=lbJMFfXUk;Mj|VpOb4eQ|B^w#2cFuWfpssTu-L zh_-;5C`lMAgEoKQVEj%JH-r{PP1WEVCR7E-xE=+K*UR!A@4P$`h{2jnWXOTFg4y^G zbAW-lQmn+OpNJ%`pCm!Y^PAcz&%fVb94hropc`?GRbGRZV7bk*XUpMM>Mg(QKWNL z6?XGu_hN;5vn14;QT~46fNg&t@JJIumzE||vlcvsF4Q~K1k0wVE5>?^xn!xV7z=l>wqQ|kQ^MLce5Rr6e+=3x1@M(oQ&j3`|@Rt+OKm6h*MZp zS}qA?TaqOj-m<8EbViN;3O4(mEd8-D6cYII03+xeLwFQ^;B(!e?NvM;vVJ0!j~4GwbU8u~ibL zu4|QOlcUEDA^RF#>bN0xt>TKZWYIbsG{gXGBSdGl5hnwIY$ZKPyK|Kga)mtbq@8#7 zIBVXRZ5YZAJRLaGRY(H0*>}7W*4CC4QC_rIh$ksA9JVy_xrt*mS2b{;U4(42rcQ`0 znE;qx;gO26PvL)u<(?F$(g-`SmQ_XURed7MVH&vS&%h7&`e5xYa=3G>vAOk_EfgBM zb%ldkniO|%5~S%Lc`^NN%;MX~(zJO-OnExyi=Im~b=67Ae)hKBlNdrwB5_qlA378H z??Z95H{JF+)S*S+@-y5Kr&@e~=kH%R2dhDMyjJwo2`t`vQqG>epjOoPkvhQ1<9YgZ394 z*a7x9-@-F+S@Eyzy`{_`=I_LrGfA*R0g4Hj8OmZ+h9W(q3?ZpXpp12C!%%`y9>UxK zmrK|mI`Z!8Twp?Q3B9}=VS7{?RAd5Z^ofBWjb|s5RBS5Nc^4>5*H$jUK(XQvn@a!t z1y`tATZF{;(PCk7tAZTm*f>BY5`GexQz3aGX-!B07jlSNrz0V+J6&I)Tc}wsDza95 z@{B6XaTC$BP_g{7jYL4k4u>u@nLU9Q68FlXc`gJXR(wCYk-ZlUdZ_|dzsjbM8$&`|72KpMRMvew z5{eeU#>`#V{W8%ia3G=9lORI3XJoBbYFyMW>94E6y@E|uv4rhn51@Lu7d3n~NYWl@ z29ILJULyna8T4SY@8-J001znJn8D%{H$}$CU+{=+15@;*^^KARhl8)@C4RENN~V>Y z9D9a)uRhN!ij=7VPLKD6Ha1F*oQk_V3Mykx-Xdfup0z$wplGpkN8cuf?P0Z96gQdt zS2#Tc;ZR2ies)j_`SD;QkLz835Z<^`V5D=9m2Uq(URZpm>SiOFMz{C<+NhZn4*xH= zdl9~ZIE#y1jfMI24fE7-hve)`Xbud*yR!cs=|&U0wTbBYVK}9z6#pbf1)DCMon&o= z9LotBYD5#Wv3o9#CQ#g&vbWJFb}?rD>V>M~5XS$cx>5+5v_R)R0tl1rl7~`)qx;p% z-9p{mu_QwqE8p)~?lL_N-$PROvClN1kyI2Kh7#j|OExU20zDtNH|TSe2nNdxb{ayV zi9a}yQ<^q>cPS75OhamfRAii@*u4&VzRxEP_n+2P0%2Y8Euu%u@#C>#3kTu*kyNXh zlZ})%M)p9{<4vXHvQuv2$|jh$)|@$W4mmvf+_$WG!Tekq4GLLv8N~1;M{g}S8#K%u zJD>E>N2n8IK7tIR=}nc7J`pVh!VAYWU4C$p4;Xhw?izrwaK*d?~Nbr-}MAKL+yhR*|T1XW-+U*S(5uD&!T4Dn6eyx#cX4!!AUDLsi3eq zeVf9fB!zBXQoRF!Mbu6DA&+jA8)Wn~M8UqYMWJf?`L&C9RX+rJaT2O=?X*^lCuUR9 zD?y6W5G`wtE~fq_1*7}O*E{7k&HcrFN{I>?ZoG`U7^qj~g_ZUVFmQH(ms2{3S2zhm zB%`MkDbks@A?>Lw-(Ovu7aotH!lJoOTR7uPbTM(U8WDi(p?Q^*7RZnn$ISX+2+_vw z9htNce5mLi!KVxyAp!6}7*6Ng*E5Q(N zQ#Y^7TMEN@PD{^Z#+Xh%PpL7vT!lJIXP&XZ+7s^E*B+O3K2I9?$HC9S*MAiKxajl; zyOas`5#4uWORrb$%~B*J*>wgD&mI2=kME_yC7Nhmgv6gKgcpc00NRORG#$oKh9A$x zW)Crc@>Hue4w64ihz6yF`m`ugMy*!Gi*l{p^P;BVcOuu^l&6X(vGA+2zW@DWgdIcR zoWkl-yuA9Genfwr>;c&+vjyIx3-~NMqoG22-83ouGkh6mLPCT_{78litD^z!K+(b* zm5`oiBAG>RDaRGinX92u@P1gM^~>S8+3n40yj`OH-seK>C%ux6cDyvxw~Fz?fm4?L zBgYY6UX8aDIvvWS#(W!X>aRl=H0!OQ7TOq|#^p#PFI+i}mZH8H8>C1&{+%2!f@0{e z<)t~2DG%j5iK-?&8F#B!p1ZV-1@!Sq-tEM765Ib^4l z;M3B#HApSAID0gS7%xISJfLrIC6nAFg6_SAnwT_54Ykw$-~gk{Ghl3AaK1x1i`m z_3`6tX=T3U(;@Ay~Xq_fB(1Xq$QpMBd+vNSA`%i9Y-!VC zZKFD#;BNccTR|Z^(CUIA@F|@4QQ*)mg%yt!C*n_Z3$a>T<0=SCIv~3s5MNKGXDS7y z(dM|n`J0nDA-*Zn@JX$CTbJ3EUWvyj)F>+KRE+ zwEjT5sC?X~6{?F{_t>})p8q@L5@Xj^676$d9|1|kgVTmp67|w|0Cji5sCd;koCWTb z|D|?6cU%sB*8iKGOFlF1|H1lBgVyZNy#;57?9HsAq`QRn(I0myIu#{aR%oN7UMuMS zbyWNFak=qd*{J&;=mq|dFR`cEvLg9Vd2#RE*^sevO!cf$$>c_#mWb!>DEaAD3g6NW zr4Jt&Q??W}R#5cJAAT4VJ@w}!H|yUzr0+pjX~j=goyWyOiTC`^?bB1+p#S?e>IovM z%dg}ohz zaCSXf?;-T*u0>$r7yg+|?gHd@TFT}xe9C9p*lqLNCR9Tn&fdyhV#P58R8?VxZ&e|r zFdXZc8<`e6xH3m{VFw%?(KKW+aW`wzm@ziX&rLAdBumh~h0wOb*Fqa9NuD4Akpt(i z{+r^av~Nd%zNEj%wY!HFS8W9XyF|ZAR;ZbHxAf2vnfKZ@WbO`i z%lWy%;Q*)m{>y?3`|bo?iM*MamZOzG!q@&N@ z^u1};;MJoUGQoQRa&@DL9mQD6MsLOx@i}x)p z#-|r8FGM`9J&j{)Y8zsyhMhzrE(hMC6>{h;HuT_|fPLrR6uZE_)KM!axYO2XFj7cK(g6Ii3BK_=hG;Ie|XLL_n27QbbTj6R$2 zgE{6h-XE3ZWohsH$6)E92<*FqYR^tHQzK4uqmQ8qZf zA^EES3Ip?2=Q)c->SWFA9rIK(2{;LJ&Jks7{qUEl&(nIVJXfp>@z5|d%JR#) z9TWMS^!v5612jdpbg1#u-A~YmiA)A`fSgloy*SB%65~G)yd$S$Hn`| zq84c*hc7~aJ}n|P_$C%HvtWAYNd%D};@EvBEPdZFD1T1DJq@LzbVpiqme`XYPl(QW zQM#yTW{sMvWeb3>xeMHJ=fkr^(scEEKO(>6U9>oVrx^Xp3zo4*gnlF&!7uQ}=Swif zGKKIq13od8%uHbCDH+WgZ#yeb6b_!GyJ<2~EMK2I01@%H}WFpw;F;ITSlxHzip z77r|EK%_Hg(84bopq@^;Rl;m!i8}lwj*2ZLYOsa~=0zcmrOO$axVcRm)BiR>ZmXm> zJo;06)Au~FXhB>B*`Plr!eilB>s z7xJ6W^eMzCquxD(2{bW&mt6SgW1~Cfa|EX7)DV9j{4k7g{|N0Op&#I(GiYp9``0~? z0xA0&96=QY{wp5sP5S!dK=N{+__vG-MLmbJyWfJ4jY}vf_U&Ka2XP;(LO1Z>3`I$- zVE8|7JdeiE2mWjv`rCW|l|L7<8`aP2zg2YEA^JmGK6YYmz6XmW{_9EF#kLb~pA{$a zpc`~TL!SgiF+r6yr~GjU+lH}!U}5S#VT-v7`yY=q68>|bLr;H?BebC3U9S9$rN1IO z4nL^blCg27L*9E;*7g==xu+5yDE(gLu-!s*-RFhU$e_e3hW&d1i7}q{T_$_a%rXZ| z$|Xb9G)#wb%*F-i(&LpRm7kk9<}%_B!)hO2-lYvlm;B9l1Nseya&W0v`f@ zfn7EWs9Qr^Ka6T_o()kUb03{ue|~zyjZ9Unt`4PGx&gCR+IiS=>28$i6}nmCQDL4f zYs-fm9-sYxq`>GA9u$>2rYAIbN31L^q)UP4EdwzqIt%!^7BW=^r|lfH>)z#L)>bJS zs+?!V+XhzW#p@XnQylSi`VD0y|U&1#vMIN@KnVLph z&%x;j%xdWwIL&OfjbiYpWHr``(HlYaeoP}eyKi9VPNH>=%MTPkyW4i_h)m9b>Ci7} z8?sOAYKIvzB6|gzN6*9&Wc*%+JBzR-7|UuG6YPmkR^dQYo4Bo>0{k6WX^10+E`4Zx`3RNT=ZVmuf}0f+>tJB8GAJ!6Z$08@Fj8dPuV zeu52$WTy#l#y@tbrn*&#FR*eXjOoLE8%@t-XDU*T<}H2*B+)}bcbAA_C@g!65L=`? zyCK9dzE^n~K*1|Z1=SbjVT?v{bMGw_90a#Af~i}y8$_pJq1k`8e0u_)xa7pbIz5nx zO(e@bJn@B-7!NWbp}5?6s0y6~lvAej!w_1eUq*b!-{5_N;IJ@gswRo@+vdubx@#9F zBv9r3U@o^swLrTZo{brOoJ;=XutRl<5si@il{2E*xX3B9L4@yq0kmY9AU@R~>464; zV>(gS28kj0_fHF`*vm}c?h{ZBr}6~V1|c??ktz=92Ql_| zC<01J#W<@%_52p7gfxTizpEOkY{EG*}_I(PQ#J-%WD(N1@xmTxeDeERNG*?lq1gFE!6(Z^M2ibN zxP+Bc%lcx!1MH+M#{J_Ux%&XK`8r#3OQ3JW20l`xjIEQ@5H=z3SIPM2#LQOG6Hcr%oYQ zJ0jCvjjY1?`uU5i@!Esy=XSl{Q$i=|AcGUNl`Id=o)ix>@E6GBHs?o{H?)DcwUt-l zeKC2`7APN_&EIOd0o8kVU{a>NGbdz?ZTv7wc5tAv?wIc|IHI;XnKs8wKHx|gB@Qr< z1JX>|re?oyP+hnO-IM^*9?=`J=e}LQjaBo(8N=*MQ6N;11SU6zLh4W7RQQ9&1b%zv@Rd=c+xaVEFnYqtf12Wg5ch z(mae>rLgY%xlcTrTK%J8R=hAYczwu&23)U{1^4a_=ed=uvAMS#Tq}lA^m18@{14<$ zw>EpSFkwUJH*+}@RIfemM5nhnG@RXKa^i34B=67OJbK{=DOPx8DRAlLd~}ac3)-r{?|V<)jR>icAat zYbx;nA~je;B|1>$edUhfaTILEeE87F*!!?DIZN9~Xxtm_M>#Z^saB`J7%>y$)4YhkQc~Eygu-U9Hjl zvt@rS(=8&`nKO-0UxU%PwutS%9Gnu#^_Y?}112VD;*=}`r9d_7mbZLHFofRg+ge1; zNuv@VddjO6b3^&BpErcavY+srxIzM9Bh=Oz_W99yRHSo5P?$!xOL6sK`Sw3p7QqE% z(e>@R?_FYrOT0V{p=?BD=(7He=v3d=1x~7kDWPw*X^6kDh?&U+vW5B+mbSP@FX{I3 zn0sQ;nq)wCdiv^0t=8*zkjQVAmfweQg`45EKJNYsFLD=h)vOFn_ZFL6Bf`qNgo|Vf ztXiEdJ}_3&$SmI7ubd?nA$&hV{;@@YNbu*VdpGi!7wAT;@S8pQGkaG{ZB6^rL(R!H zFOuwOR{T+k#iG}jiqhURL30O1`DoY2`1c?j4{h(`mC`=F$fpUREgTFFb>3?`F5XLg z_s5HfCn{T~V9U#reC84-v!|L1>65&tp{I_N>$6Gn2gyf=Ydm>Yg(F3>6FU^yMe!*4+B(B$or0&QgEn;Y#p|!vx31fM%{_R(Pi!xJQ_b{MQ17#;65XmlgE%ER z_{`18IA>oMr7qQZ^y#yzF_%*MPWDlFrt;5y>&McF75*j9Y;2j9TD-Srn7u?0>=Bjf zMrhX{H|_~KWCr#f+tPqelI(A2d2R?}Q%ScYCLSyfLNjN7H|i#j;Qy=^yLD*@%W$w= zs_&|{S%@rh-JfQL{u5wbq*Y^EX0yOWM*DQsq3(LPDdfZ785h=-R&8z1{Dl0u+LLwd zGyg!{iWm6lA?+#FWr6a7{4qJV3fwq(0q{(_1D3WQEGKQy-w$}*j#w8yF)BW;Ji>WK ze{R17Zr>g+tv|4i1l*M4W-?!_Xx_1woJ|c=N3otKEY^Pl+bmR5XzfoIX<08+qt1EB zgB-p3_uXaGXyHx?RKIP+snNSin%CU;HbOu~W(CfntQ@!%Cn4_Lj+(>O%#IfifShF# zfQ|u!$+|kLQ6q={xIzGn?i;f5)h*Cm-zQ4Z?YhL$pUe?ar2OArF*|Y&_ng_WM5bud zayX|JyaLak5zLiQ#g9@-uH#Wsro~RBQg;dHx`%B#p-g_$IRFqoAd?#dhGIt~+~HO# zv1N1JBJ^@J&R)?hls*u35y9JlT{Vl8t?g1n_I+(Iq7+MlN5Fbpj#n=xbQAOQ0f%_# zJ&a|^B^U=p6qcH!QItF_@0w$B1#C|PZ){OVkQ%1CtCTVDtUR38EI zU6N|5%;I$p+WkZ{-u-Nd7=EA#d4n%xd+s3T88FwyB1h+k;8%aYGf1AP;dr`^hlm-4 zv{ueh8~C`%2hUHKB0XXIm0TXfd4!|oyJ;h>*r#14{OLV6di1pW8xxjoUE^ir_+8tc z8%FD!05$M6p|1g)YX=g5@@)6*sr$)*_VATc_W&Dz5{2pMXydqv+7j1z3MRl+jBh76 z)kILQ6HHrrW9$MFrAal(y!K>zQ!n-D%`72!f{eop$B5}$b(g1C-1r&w1d3JT$AwiL zey=pejfR+5+5}yV^{IKpH730Q1ca+3~pm=wnbdf!IbHXW85Xhin~H z@;Pu&F2|zg2B3|6`+?z-&uGNkm;GoWF>jSdibya4mkQ|lqf#rkw7UPyE4EA~GNX?7 z?wc1pT!8CM41PHvbt&D-I6U7t2ICFQ)nQ&M?I}I0F(Ifyc&3-+eIFf#jNHM3BFwX{ z-!m5~aX5TuCuYRTGt-YB4-+lK(HW1zh?-Pp$q7H>I;Js0-?RW%89D@N=~MCx3kY-= zD)QqUvk6lc2{Pj13mF)^C{fD{kRRm;>h-lS1m1p#b(G}U78)5;MaS$gd(RxHcRqLe zK#%yHE5Oss)qi)91I6mY_dZj=>V0F6>%N=d^$p915lUl8sS7cwY(De*N0?ZY4;)Jm zFl0XrQL)K0OK2rto(#MR`H-f#rA$A~c1m&QVd(dwMLSdD5);#%P4M!Eld&C+{uW(d zWE*pl(#5wM=-m8%bN4Jo*Z23={B)4HHp`D_uadpW9%Ask7)p?}g`}mhBy96lb zC^!w_ST(;2nHSS@oczRIV!FSRx6a(_Cp;TM;(eP)X?PO~^d6tj`fr);x0nM=C3b@~+&0zeCtuH68vHam$)DEI z9??I&2dIJDSaI{~=tXe)(zFaZz8u@Go;&>6r*KeDQ0$=OV(WkB68X5a@*>e9jHWkK zE-F~_-FIIkS-I5R&7|LCYMfo85dqS(t8p`$8U-hNd%DeU`|jt_%4>P#nBESDCYU;| zaPw}|L=A?rC50PNP@?||mdB9^1IA^tKX4Sp$!Y}CLnwSA^tG(X%g!XGYkkNo zQk=;t^5zbF;JF3MSu-Xe*b%`u>_FX+=DkJ2dZNr^nBdzvqi4JV#80maJ31xwnRE!J zn)`hKjME?qH^9KW#VI66nEOoGA;~VL(lMADaO+L;eQo=RxuXXt_Pt|}CJ-}fWJ|WC z0nXi;CtQ?=M4~!%oyV`kz}SlSFVon+n{w&Bz=U2FKVMKnuW=<9G3b?@0UHFpq(l)g zp!cEzkY7LFM8$pge3OXb%kxc6)&K1;{_1le**Jyppq+KC@}QMplJp2a3)egCM1llG zf;D>rcd!3hnYold(GNTt$;Nm;@9fbWepW1F!=T$FG+miuRHr%~_(^MqI0Z@|oRv$e>w;T*-&lWoZszs}fE45UFj{t#<706f!Zh zFyDAqC^~H^R3LXZc`q_KztmI0Ub59u3h2s$5H9rm1pOIhi3uA5!Jlsk+FnOcxgNqx zc3V0}=ov5||JYO*|=}XBAkV zS&@ai&C5D`Eq-L)IUHepPLTGmcR4JVt!fnqEgi7E2tOzso35#t8 zYwv+qi-&+*Tq7TI(28t28_<9p$&`km>GGzHMTPNczH`Pmd%0o5&B_w@klDqxA9bLp z^}#b#OJZhtR~L3pJ~;+MiZFw3(rDp`(w(9j?+UY`t1Iy7ra_Q4Y9vWtY6u0>>*+D+ zg*g-)E3#L1DPHtarZn;(I}-*fv>A)_)Udv=J_6qz95&6d`I2p_USNi_oeU9un(= zXJ+CTW@z_Z97naJk)bhP`w^n*l~z@zn~Uj8>V!=8zsT$w{4y+WmaAaMH-|$Uzt7uS zlP1(l08Y1Yh>$GWGWe$ak$KtRWi^sPli1R_jT)Os!5W#-m?3dvz3ftfa={GUrBDaQ zwUw#aP*+d;>#()=&B-}Wp|U-_fDY-T>)RMsvt6O}adwUl`|&LnBTbDqcyBm?PV3i` zk}G$7(cBDfm|(n?Z$0;3$6U=8Z5+>DT}i{0q-RR+>gVJWkdV)Lh?f@C7IO5O%&zAI z(@PKmF5vp+=7FMlq{qr){dP_~yqnp$;s&}<9}5zpCJC(p@$xoQiZ=};#>NiGel~7# zQ}Ysx2%xlUf}^sJ3#FQQs^?O3c&qDb{iEZe1;S!t8yN_*ay~zD7BPo!M}WW|xl0hI z(NGw#wCfvN(vfW=0uf=97TwIFQ?#`CA)-nNiz>Gyy$0!_ZL*cDqdScvt|GO_bccLZ zZLj6cH8mZtY~{vw_&Jn-<}m}T>NOu-gKq~2qANRWuNLfuuy?6(3rpGbS`iwP3Iw9N>86dU&>i=V*6PO|ffQEYXqF{Md9bVDq+t+y#@5KOV=mRuFeqI@V zN#Pi0;EnB>pZCgwiQ-!2(W4@%!4yCSd6|-RJ#du7?p+)YpEu+2DgHjr zo{4j>QqBA9ntEL|>b{3uk^gdc4D(kV)5`rhk&ji3v1xLKaZm%6q?x|Yces@y$h-rG zD9{*-3o3hBSDcS4QUr2IZ}74%i|UQk5WhsNBfaR)`cAPU!2YKb3yNvXjB@Rqdk6_( zMd#zP6MP;XFIi1(PhLIO$n3198Y3}hjfC`77~A(?qxCYE!Z_<3Ulyw$CO*8TYL z(L2GfJ@aL{<(Ixp!&ev(@Q?#Z`x@IbC{mE1QE3hQWm@(3)7!ttWlNFA7vRp*f(PI; zKBp=CZzJ-52g={_hWmnap;+Ba@OOai5x)L9se~RXYy|p$Yu70Mnk<5$mF5?V$=qk_ z;PMEl&-Rjsy-1>q0m)q-I+670<{m&p4dt06MXM_(g2{XuU0> zve>~5zhnbvos55=3jVCR2dVd9YczJSR@ncH!>+PrVJ3TH3e!9=?;N|>&Q}$NH_wMe zKt6&|5gDjSq~b=&XJ&nAsePgx7lmYFQS27xw&^i z3!rS%pCugbreQa3SV`UXC@9GZc3I4X9b7@ZlBdXWB#G%*OTv>LKkjcgL3N8DXwis#ydWv-|mTLxzj&S-RmbX+Q<9jRYhf>SSs$Z z?ymJ$OdF!VWH-W-eSP0+C)oIo!_FOmMBFt=Z@n6W;}A1vxjQya-L(zFLJrL_*y* z16FE*c#I}axgGWjqUB1ucv4Gm>3rOSgBJ(t6+E6(Ao-H8dpLagHe&LX@t@vV1m zSaC;|STwr`V$i4W)+uH=5J(SG<}QVVk@UO`oJ+FmVCl!gRm{w{Q3jkTugS{4xL}bg z4<|D!UuR5RpCm1qvx8!2$b@OGK%zK=$;o-NTyldD-$u#_Nk=9mqzkSbJn!NE~NPgIzRSJIz;b63(N`A`YDU=@Q zlnaGNtD)FU)OEF$Un@9^cq`z---mO2xXR@q2Pfxgk(rz>b)mJ)}oOqhafMW2@>7ST!==AC9!FDP!&k zyb&DAPzVToU%9|_*k{XU9r1BaR7`L%oaSXc6xCx+p(IAiX8T9d}-j zVD&l|iy(88#DfhY7JY=QPX4?2`n!>by-oV@E7WdXknEV~2IjVkMsLN~m}LDDge)TI zmK{$nF+&v|e1Ahm+}x(oI6?p*t|b;c>wDNk#`ZO{Z;;plYkGZw(qBj7Tk-)>M=pTl z6XfKZ<(k>1V{tWUARk5#teh-ynHzVP*BoK{gdAxWeHV>$8%SV1UTB5FmjG7WJoA}R{=D%M)sT+7s4NuEL+bnHp&D|nk z9u@=zDbMcZvXjQ56c&L^Ugt@k1dM*Ogy@Dfvl_Vw1D07%=@F`)K}v;z9V0y%s_9#E z)ZxSIo}ALX1cg%HF4h3bCGRr!sLtrl2cVi0l~e?PJsTmgMkoP;O5B(oNQ9y&g3ZwU z5Iq9#0MFM8RKhco;T^5|eSHwEr>r(YOCNHN#Iv0Tg^p3%Q$fbzv4R%JJcPb}2sRKc zC1;0FB!7Ky=nAt2wcD0E3p}^C6#zmOYR7j6)dGq?;;xbav;qj#o;l)f$Ykn-iuT0o&&WWSdDJ^LVl2fbJ8bJfCd_>A{;HW+!-^bFaPQc3}%!pZ_ z6Ou__C-s$-$MCoFKDnFbdIS*=4T$X!4cogv5e`>&*Y@Zmh(a+kWIwtT)zy6pnWacf z3qV=^>X)qX{k8=!sLOslx85=Qp<6)$LQ%@3LK@I^Yhv3CtbA{5kP$Q&MPsh5MvHDg z4RG*j3=GOIceG8Fv7#-D6y6xyRA!t`FG@=9;s+dhtKZZJF0U7nX1R{D`JB?Rqx||a zuIiR<3b)%)7G83zx06Y*6vOu{2zI;sWb$JQLy*kA)npN!@`BPp zn*L|O_M2Pf?-aFlj6oYc(sTueLA)F4f2X_;OI?#G}&v+y4KQ8 z_YWu6F({_(Yx8|WGjee@{z-h$;Gf^v;rk;@Wn97vm%zv{OP@eZWf$w{QfH#LEELC%6S68 zCMC;cLg3Iwvz=qF9aRFM0T*eHh@ebWq*=}d1Mz)x7sB$)ymu-i5+hUL2e(h3SXUA^ zyls#)wMl3M_{4Ul!nd1GDMv-LB)LWN5T#}j$Fl%&;iE10#P5J@q|)Q->&}vZwC?{e z@AmecL}TGdDtC`)kf^?X;a(E`UfoQ3ZZ%FlzBeqYMcF8ew>Ny!Ktj^PyynZE$<=i# z6;D;~X>FH|kNa2ACf;f8Zx2Javf0yDK#jgqo+KLM7bUI1%fbu^;K5ufg68*R(!=c9pdPlFt1$}h-z*IiQ}{i?`GBVh5uW1NcS zu&_VwBe+r5;hZ5n?4Zf9k9?fkJ~#HC4;Ht6uDdA7r#q>7Sxcf=iTETDONWj7O4OuH zC<%^E>>RJ|e;0NE#TaD^@nvh(zI}}Xn7SwW8~QUS-%0fBf)^Gy)z#yN<;zQT4YKNf zkmkckNRC%(t+Jqwdv@mmx5^vKg=Y>(f^&Ww?;~j@A5v^I;A}aC4wWdMF=h4%N=R^Y z+O(FGa|m?LZNipJdwq1J-X$iZz^-{F!FtrcNQNuA3n=#OWz&*T&EaO3SIW$-d)MgL zGT{e@X+!p=#1TKDgpv}in{MN%7wei7PZ;?arY4cC7CS)R#eQJyI2f3zC^=LT!A&mxh1W`$+W3x!9mETVjVabry2iRMc#H-q_g+x>vQVT2!IKLnXn?!Ct zsUDjnVeyWMVJYWI0#xTUl|ZCo7Q$*pPrsteptLi)m{9Js=MK% zY?(T;5Gr3_bA!>MP={93_d38+j!u}NQ^3~$5{NxmvY1}p#{aF;3rbCHutN=_D<@Jf zN2LUKwa87dE?DG_Z2K*{3l7-El8Xl=@w|ex;EVtPNFPk-U4yWH@JO2J&(pxqO9@{8V&ShU)qoZN*l3oXOI?7xK?uZH2thQXgP|+J{nZ1wo!dUy|MlJ zP(BczEnfy(c41JzjtN+eMOFm`JzH%=#fu0)+Swf}0>qeu9_y6n)GgT!q(C_(sRtVZ zZ2}0QcZR!+8N?-!Cz%!ety< z;i+lu{qh{O+a$!fVJUr_>sbPRfLgC|CCixnFcPCWU6x7Iv0=gP9%vG;Pb&9aXnX`X z@UAp^EWrV*jcKk;H+)EU&8=Mos&6tOy}KpSUZalxgR1;r#FqbgZ7vV3x+Cy=93oi} zp?`|D|2>bsWrYmo9@hV7WiAc=C98Qwu9f-M!kem<2Kf#8g!es(WO*DtwC@)b^q~B* zG(j$Ca)%x*5#<7%#eQNvYP}e67WbLAHw66bNO|L(ra(++xqedd`vUz=PTnL)69g(5 z>~H7YI=904U%h>IR8!Btt)leadr-O%Is%5?q=SIcq_@zKUZe@qn}GBt0wOBC7YV&X zC~D{(0@6Ey0QczkzPo<+zI*?AcfGZ;PUa*hb7sxV+UN7xd(VX1%cbFdbnl8Mlqf~> zEq{QX)XzFL)eee*DA_hn2^U@sA#ZkgJixF+MI)6Ww(J{xv-LNQrbyk@q_)&z8R8Am zQm|$P??qC)L)!8M$}!S6;Eg>1@7{_hW$@Bd+ri22%)9RRVNzg+nkULX&B z)F@HJr8XUS0@sfQF(3y|w_7D%XAu3O5f|?7)X0Yq1G}M}F$_@>qgGz&_lmiH(mcPQ z3_AV#Rh?}+=ZRo%7QQbgJCTi{^R(;_!2nj+;NTkVuQ^+SeRsp`(JGm&X-5DO5s`I} z-YFeQ0A=AT!53!xOUGj;(AxdIr6EM`#iD5_G+Dr@E9_7eeKMzpRxji;B4@M24 z4L|3awD^$YM_xFv%Jn;7?6aqPL~b%MSU5T4p^b1m7n!Lp1#d?DZfv!PkhAkUmk#7h zx_P(s&F(~S&znc6z(>7i*TJZU_2XgcFi)w|;N!$?(68Hb!Bu%f_f=}cvcOEn!;(ru zGqDDjdCS>)X9<^^`N)dBnW12`cN8CethDp=lF<$2jnlcQd{={WkO) z&By(T$K__(g?!JgnZ0Oy-qy zsPPTE)RAA+OiIzpLxQDLM1TIv4^-;aLe}k53UXAv2TAB7GApEUGnSwN&%$a8<(D?a zF_Af|$}o{1sz3e!G{}fNpBgs2zH|4}cVS&!+8G{DPV|_hRJf=}@6-a+w~DPl!Iz_S z&!stK*I;g$%D~__nvgIlAv}a+KJv=ocdl*IW-`vvQWi^5B$o11f-x2S9dJr99cXu| zAK7PP^NRR-$P33?5Rw8=#3?9%e1tE1#it|~KG*Y2glXH{!v~~V+XFx-{xgxwqTyjc zURIwkWEwF&{YRRzmBYQXB|ky&iXzesGxDL|ZZWKQ#A&m#*(QzqdYGxIeABDKYNw@& zvtpUnI;Nd#rz3o5?_B_$Qlp}IEd+xYo7-4q4cy3zNvlCDFYKh&(X7Z;m)=PMaKIik zPBGT3V>PhNhkmivoc5pY$R6RV`I8_>cMSt7erzv4kggK+rO+m{I5N!oM4?a^CfI>4 zR{sB3SW5hj73P`6{bA0adZQ`m-WdQ*f z>OmSxF!tEy>Wr}1ybMWv`AA-kxa_Yg@~hkS=qq?V;niqxUS=tXIRf6z$0y)cu7Ezc zGw^O%(wn|YB=!aT9=GzC?~g%HH+b)8o{kbuf;U5B#UGMLOw$f6Y<`m%;D-94KbrJi zJ0!D^RUWIMR&E@hEYTnUOwJs*1xwm6k?M&CsM{`M1})3gF1Jmd_wBhJCzgj}2D(Bd8 z8f$p&vill;^e?)h6zC%(+)tJw5f2^?6+ibpw4!>ipJ7&BX>m*x)=^^Q<0jeVY`tyR zjrEkapJ?K599}klKQrl(pa8y)%{$5Kp&x!q`wfjU_>9ucoyJ;GiO>`=$i3qfh6@)e zL_!K}*29!PVj>FMcG4p5`}Sg;?GqeaX$saZcYf8=E#n3?$n zYaCcJskW)h#-=h|Ai^L=)#A7Z5P$)xcN*Z2Nr)no%GUomHTut|0snuVzy0@)48+eQ z+g@)9z5u4Jg4DNYrS~mq#pg00*fxd!C!MJPQ?O-VNye$O)8FsG?JNVu@A#IKe9U9~ zetnV3=ivUK3AVKQBb1{m0rD%zFFs&=?1^BA7rR9#c3LCTuizCNGcw;9qY)J^X>>p3 z^2h}plONBW-0_Jg$FBZnL~Co-Hd@#8@MAIVWuM-wrmhe(9v#+^ozz%@ZlgvsuSb1L zt!jOnl;u1L6U%uv{4Fxm=Z}ycYiR}0-*|`$6H87VO9%kPf1dRcy;;6LeA&{DDML+I z6nO9Cw_6VD0I+J=NSd{?e%e%fS{cR9?-E5th$a8|bxaRY zr94#xNoklp)vqH!3|5)_8{%ncNLc<4Zl}hfF*1jaSLq#h^we;M4n(>b+Y%EK^}nwC zay?ktb3I@_kJUL2Sg>_6as~)FuR$+64J?5+=9hkI_^-|{ofgVwQ92^UCNH~z>G ziB;^NN4()^u2lcrDr8#+&AI)E?kjRu9#96Zw*VkDOl-){(E$h~@tI4lNj~Ru_Y=sK zU~UzTp4Xe}tBt=o&r9;eWQRBCEO-J^OW<8Cz*@F&t0U6@KTERu;T-}!{Km zq5<{SC`2!nWY0E%_q7-gMC#P;gTIz;;?3$DLm5>}Jjs}7ey8F2hZR{0plZJyOkeAZ z_XuiE5{?v=q^Q2=zSZtkH?mTxOR{If6*^~r54>w*iCTuyQ^Jlm#EaZD?`Qkk=Ewk} zk*%DkXWyazsJr(%`_Ch~<0kMVH=vPqh)C60V#2ZaH*7wof2CQ~KL4A-9RBPs?BuKQ zZ+e@W!33j1j&B@=f5D#zn_j!|3A7~{`)xnzxB~LxHDk;wTdN#1qc8KC&_<};6=<*oM(aH z-OI#j7VzwBH&4~aJMkdAs+{SkjmozzXGN;(j1hp769|9)fX)A!E9d{TKSUZ&s)7%+ zG=1xtjF6}Ymb)@z=gkidAS3|bTLT7<{W$2Dtb}8u@oZMq*YRE!?C8Fh^!HfFtKjy9 z5}x|3b3P8#Hms#PJD_%>Bm#AHe07nv%;HyZ8@heUF@2r;0Bc%(VVh4NNpfTeHvi$Y zbpB*7_|3jFefQE~c6#t|_YP$cDgu7yy1$Iw#aDU04X|^Ld>%Xd)##(yl8dWV_{1RN zDWcJLg+{vV42zy;)^DRC!9Cy=by>JV0DUZ#_)`K|&|D1Mke9t6DW*`dDY6IL3=KrQJGLOyf4s4XWrcdnfc)PV} zypy~oxczWEP{R2kQ{U^{boS~T-|3Qbv=(Z4lFH|&DQK;t1~Ii1)>aLTxLv*A1urba zMpu>=-H%2jb8feY{bn%Gkcwf@YB#*%c;hL)*3Qk{KBe=++VB8^eNjF)|CK)D>-Fr) z>)C*P?5jMfu7})iF86~*v*(~!Alo4S z9FsD%EZ_jVCtiynPc9J@x;+WQg-ow}{<(W}lwOO*sw zXA>@+^}06st_ZvCK?aG>ol0o=dCaU@maTkSt1hfI&=z?pO_DIDtV`~k(VL7*p+3}A zCdX8xcb1iO-s(U};^CSjxivKKjOmuPBrim?T>agM=9vGDSc_3pUxnN52^g_GQhQJi zbViP$2(rJ%XPUFHLvF*(p6CRk&UbnrbnO|~g;B1pgB*$6K3i=Rg zuef&p1jkYy+eCVyQ9HRGlWz?`1y}=pdU`Dg7}Y#cPc(qK+nYmLBhjFNxhmZ1^m>n=TW?eGT$`0sB+d0S zr&g9|lQ%?dPXS+=b}@FkQR6se5m_RQdZ_hMqLN)C_MFaxqvGIKLnLatG%#R(Me_Ce z&c0a|S?^s% zFh~Ea#D!w41u%tBXFhEa%fJjVK|kc!C_kKSaG9*We&l_-#M$+_;dpixu>N18HJ0`_ z8|x!8VbY_#H{new(i_Fwur5>dm$weq%iX|s`y79^)t?Qs$`F~re6xBR`4N>RI_WzXN1x~kX|dp{ z!CasKwtSuKENXATd?vwxIYxsQ=P5{GE{V(8PWa3^Fub7um@<~Tq@zcCa*2CX*Z?x% zl408$kJO3F_Q@v1RS{{Kww8VJ{roy8?vvd zYPX$tt^@JhWGm{sS99gTjw!+=PU%Fi$Q1KNIF>)km3nqOus$p;*Hi^FLW*KuoRNpR z1ZLMwaDCA(t~`5B2u+BoQuK1hAWVE}ky|L6Hu9UQ>Vrn{bIZD-BPYJtgcH9AvGhQ+ zdH3?F^my~DcI9^?)C05ZMiTKO++C)z$W*!nixmF*e3;*{#8dBjSIO+05MZHM4=%;t zs2WkXGUL!f#7uJTFk@^5%Js$F0ZEe|fC@f*z8eJRIKcA6BKe`8X|z8^{M@pXfjiug z{e-k_f7P?z5Urb!kNq(^a!v0@z31_bA?`dY1_A2Hh`MEV>$2CZm};D%y!_C+oYI}SVWB21Sac`c_@_56E|Pq-Ne|E{M!T=SK~L2 z3*&e5fl-fqZG2VAjOB-&4xdS{ymfpd-*;DD-%kU-XZ)Jw()3QVjDI4C!}<8>l9wH^ zXD_g{cC2zj#PvQ=?j8;crJ|y|)=WmL@Y?TIm16HVSyKrzzDBBx#;W`d?EGp4$J-VVFGVsB%5T7q8HgoRK$bO+#wyupIc(`69Pf3 z{5po9-p8hbmI5aq;#VvC1)Zv@o(uGU$O=2nt2GFh%W3V9H(|9%YVfrbf1M^Ux^T+xofuqBFZ_3@}IXN$U zgYMuInNb<%>X=G)zi#Y^C_YU1(5GUbYh@ELug=g>xBO(*LrZK=o-+yR8iWyl$1Gkj zPdS-;)O@_q>$z>ob2-s7pi87SKt2kj7_u_$@fMEpO0iy8#<8Q%%BU*uC~6CMri!Aa zppiBSSEnDrmDJk?T6f2e>!yfsn(C0(QZ{7&qHA?{NGeLPXDZXqabUCqPPgl$n)+s|-0jucOXguyw5Z5JAwl!*L4vJC)d6xp@Cr)rej;j}%J4>kkkVFdEL3UiyT{qDL4xX*=Ort4#^hL^q&`m5ldwL% z&+pkRwn_6v=2F5G`MwP@AoqMV^-jtBhxz@9pc6_qmto1x%A;$#|NHt%3uM&kuE;4c`1>wMgXyvN-lW80~4$MifWk61uTa}&aRaDY*c5=$q#OO>y% znZcFg#TcM|Uz!eL3}N}(VaRTp0BFZmdI z#{HeBPtbRe<3 zI*}SZx_L)kamJ+nVt&ktl4gg>`hd3(n4(nKsxWuX1ukARjvd!ok@;z3hmB>L7^6cS z(V#DtJb_&rb!n8u6}@i8N{sD#lqsj;0*SJ;s03e)H@JV7H|d98-)F=J!)&K3J<9)k_QuX#lMVK;j4ev&-pp?xs;NfMLFLlYSQfi zB)q^)g9Lt^dOLd9TUj??Y*>k^4Jqz;;hi(19iL!gF86)q+pnh@W&E_Vm+!vndaDw~ zr3Tva#QWU;*L#TMj~}ZnspF(X4UQaMt9Nnu-n=SrVIi8%mxoG8J1Ig z{Bmx!+|Esq+mJ5SW&>20F#u1|$htpI1&xO&#S{69L>AnY+9%^`rWxNkIk=bd!KYKU zOBRO-&U7f?5AJC+D0cUibCSZPrc9WX`W!%K@vhd20hCrMEVo7$>hfLvJJh_Qd+dM| z1O29}o&HFvfcYtCej%itOss@5;R+9fX_5F{P-N7Is+)}cm3W)cnRa1{I(4kII-`J@ zbx>gHx@#K-gYSqwjxu*8A}Y>j;Bp_L4DVPq921l8JToei%#njaZ2B0t!uykF^ug&hdxMEETR#f6$2Fa% z-WY^i^5rvLS@Za4j`JrQPdRJgHO7fXU*U?!;(O^$q}M_}f0IpH=IuE|E18wu;~F0z zK$ZC=H-~I7qlR9j!BBY5_xDWv(sT)xMBg1xUQf z&F#pGdT=rR6BpluGOmovk(`d?)lbfCqb#k4CO}H?^XM9p0K<(hzM6nEd11==M|3|PfVXz^Q1vt^yT`MA$f-y zD7VmAHI;q-?%R(lw)ot`tSmT7U$mD7etYCRiz#y;J@t()Ud4YZVQEdN}gYP-~Cf^`APHlB9)0UI{AM}%`dE!|0y*%5d2fcmfrtDWxoNsP5IWq p7@>H1+u>zW#0URRhnDl;b-f;Yqt*ay;19{}s3>YGl*^fi{u|kGpacK_ literal 0 HcmV?d00001 diff --git a/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/expandIcon.ts b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/expandIcon.ts new file mode 100644 index 000000000000..7f249c6b94d8 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/expandIcon.ts @@ -0,0 +1,62 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import PivotGrid from 'devextreme-testcafe-models/pivotGrid'; +import { Selector } from 'testcafe'; +import { createWidget } from '../../../../helpers/createWidget'; +import url from '../../../../helpers/getPageUrl'; +import { testScreenshot } from '../../../../helpers/themeUtils'; +import { sales } from '../data'; + +fixture.disablePageReloads`pivotGrid_kbn_expandIcon` + .page(url(__dirname, '../../../container.html')); + +const PIVOT_GRID_SELECTOR = '#container'; + +test('Expandable cell should have a visible focus outline when focused by keyboard', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); + + // Tab through the grid until an expandable cell is focused by keyboard + // so that the :focus-visible outline is applied. + for (let i = 0; i < 10; i += 1) { + await t.pressKey('tab'); + + if (await Selector(':focus').hasAttribute('aria-expanded')) { + break; + } + } + + await t + .expect(Selector(':focus').hasAttribute('aria-expanded')) + .ok('an expandable cell is focused'); + + await testScreenshot(t, takeScreenshot, 'pivotgrid_kbn_expandable_cell_focused.png', { element: pivotGrid.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxPivotGrid', { + width: 600, + allowExpandAll: true, + fieldChooser: { + enabled: false, + }, + dataSource: { + fields: [{ + dataField: 'region', + area: 'row', + expanded: false, + }, { + dataField: 'city', + area: 'row', + }, { + dataField: 'date', + area: 'column', + }, { + dataField: 'amount', + area: 'data', + summaryType: 'sum', + dataType: 'number', + }], + store: sales, + }, +})); diff --git a/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss index 44d85b580a69..db58aa1d78db 100644 --- a/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/pivotGrid/_index.scss @@ -18,7 +18,6 @@ $pivotgrid-grandtotalcolor: null !default; $pivotgrid-accent-color: null !default; $pivotgrid-empty-area-text-padding: null !default; $pivotgrid-button-top-padding: null !default; -$base-focus-color: null !default; @use "../mixins" as *; @use "../icon_fonts" as *; @@ -128,7 +127,7 @@ $pivotgrid-expand-icon-text-offset: 0; .dx-pivotgrid { td[aria-expanded]:focus-visible { outline: 2px solid; - outline-color: $base-focus-color; + outline-color: $pivotgrid-accent-color; outline-offset: -2px; } diff --git a/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss index 5fcfb96100e7..0bb559809a27 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/pivotGrid/_index.scss @@ -30,7 +30,6 @@ $pivotgrid-accent-color: $pivotgrid-accent-color, $pivotgrid-empty-area-text-padding: $pivotgrid-empty-area-text-padding, $pivotgrid-button-top-padding: $pivotgrid-button-top-padding, - $base-focus-color: $base-focus-color, ); // adduse diff --git a/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_index.scss index 46c964e274c3..82fc3767fed5 100644 --- a/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/pivotGrid/_index.scss @@ -29,7 +29,6 @@ $pivotgrid-accent-color: $pivotgrid-accent-color, $pivotgrid-empty-area-text-padding: $pivotgrid-empty-area-text-padding, $pivotgrid-button-top-padding: $pivotgrid-button-top-padding, - $base-focus-color: $base-focus-color, ); // adduse diff --git a/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss b/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss index 740d97537410..7d8d65d7f38e 100644 --- a/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/pivotGrid/_index.scss @@ -30,7 +30,6 @@ $pivotgrid-accent-color: $pivotgrid-accent-color, $pivotgrid-empty-area-text-padding: $pivotgrid-empty-area-text-padding, $pivotgrid-button-top-padding: $pivotgrid-button-top-padding, - $base-focus-color: $base-focus-color, ); // adduse diff --git a/packages/devextreme/js/ui/pivot_grid.d.ts b/packages/devextreme/js/ui/pivot_grid.d.ts index f12adf2f7dc1..eefe9b7c85aa 100644 --- a/packages/devextreme/js/ui/pivot_grid.d.ts +++ b/packages/devextreme/js/ui/pivot_grid.d.ts @@ -55,7 +55,7 @@ export type PivotGridTotalDisplayMode = 'both' | 'columns' | 'none' | 'rows'; * @type object * @inherits Cancelable,NativeEventInfo */ -export type CellClickEvent = Cancelable & NativeEventInfo & { +export type CellClickEvent = Cancelable & NativeEventInfo & { /** @docid _ui_pivot_grid_CellClickEvent.area */ readonly area?: string; /** @docid _ui_pivot_grid_CellClickEvent.cellElement */ diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index c53a6aada76f..471233857887 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -24017,7 +24017,7 @@ declare module DevExpress.ui { export type CellClickEvent = DevExpress.common.core.events.Cancelable & DevExpress.common.core.events.NativeEventInfo< dxPivotGrid, - MouseEvent | PointerEvent + KeyboardEvent | MouseEvent | PointerEvent > & { /** * [descr:_ui_pivot_grid_CellClickEvent.area]