Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions e2e/testcafe-devextreme/tests/common/pivotGrid/kbn/expandIcon.ts
Original file line number Diff line number Diff line change
@@ -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,
},
}));
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ $pivotgrid-expand-icon-text-offset: 0;
}

.dx-pivotgrid {
td[aria-expanded]:focus-visible {
outline: 2px solid;
outline-color: $pivotgrid-accent-color;
outline-offset: -2px;
}

.dx-column-header,
.dx-filter-header {
.dx-pivotgrid-toolbar {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ abstract class AreaItem {
span.classList.add(PIVOTGRID_EXPAND_CLASS);
div.appendChild(span);
td.appendChild(div);
td.setAttribute('role', 'button');
td.setAttribute('aria-label', String(cell.value ?? cell.text ?? ''));
td.setAttribute('aria-expanded', String(cell.expanded));
td.setAttribute('tabindex', '0');
}

cellText = this._getCellText(cell, encodeHtml);
Expand Down
30 changes: 30 additions & 0 deletions packages/devextreme/js/__internal/grids/pivot_grid/m_widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -883,6 +884,34 @@ class PivotGrid extends Widget {
});
}

_handleCellKeyDown(e) {
if (e.repeat) {
return;
}
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() {
return this.option('texts.noData');
}
Expand Down Expand Up @@ -1074,6 +1103,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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/devextreme/js/ui/pivot_grid.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export type PivotGridTotalDisplayMode = 'both' | 'columns' | 'none' | 'rows';
* @type object
* @inherits Cancelable,NativeEventInfo
*/
export type CellClickEvent = Cancelable & NativeEventInfo<dxPivotGrid, MouseEvent | PointerEvent> & {
export type CellClickEvent = Cancelable & NativeEventInfo<dxPivotGrid, KeyboardEvent | MouseEvent | PointerEvent> & {
/** @docid _ui_pivot_grid_CellClickEvent.area */
readonly area?: string;
/** @docid _ui_pivot_grid_CellClickEvent.cellElement */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,103 @@ 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) {
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.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) {
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('tabindex'), '0', 'expanded td is focusable');
assert.strictEqual($collapsedTd.attr('tabindex'), '0', 'collapsed td is focusable');
} finally {
clock.restore();
}
});

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;
}
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('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 {
clock.restore();
}
});

});

Loading
Loading