Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { setWidth } from '@js/core/utils/size';
import { setStyle } from '@js/core/utils/style';
import { isDefined } from '@js/core/utils/type';
import { getMemoizeScrollTo } from '@ts/core/utils/scroll';
import { foreachColumnInfo } from '@ts/grids/grid_core/virtual_columns/m_virtual_columns_core';

const PIVOTGRID_EXPAND_CLASS = 'dx-expand';

Expand Down Expand Up @@ -139,6 +140,49 @@ abstract class AreaItem {
return '</tbody>';
}

_getColumnIndexOffset() {
return this.component._dataController?.getColumnIndexOffset() || 0;
}

_getRowIndexOffset() {
return this.component._dataController?.getRowIndexOffset() || 0;
}

_createColumnIndexMap(data) {
const columnIndexMap = new Map();

foreachColumnInfo(data, (cell, visibleIndex, rowIndex, colIndex) => {
columnIndexMap.set(`${rowIndex}-${colIndex}`, visibleIndex);
});

return columnIndexMap;
}

_setCellAriaIndexAttributes(td, options) {
const {
areaName,
rowIndex,
columnIndex,
columnIndexMap,
columnIndexOffset,
rowIndexOffset,
} = options;

if (areaName === 'column' || areaName === 'data') {
const visibleColumnIndex = areaName === 'column'
? columnIndexMap.get(`${rowIndex}-${columnIndex}`)
: columnIndex;

if (visibleColumnIndex !== undefined) {
td.setAttribute('aria-colindex', String(visibleColumnIndex + columnIndexOffset + 1));
}
}

if (areaName === 'row' || areaName === 'data') {
td.setAttribute('aria-rowindex', String(rowIndex + rowIndexOffset + 1));
}
}

_renderTableContent(tableElement, data) {
const rowsCount = data.length;
const rtlEnabled = this.option('rtlEnabled');
Expand All @@ -149,6 +193,12 @@ abstract class AreaItem {
tableElement.css('width', '');

const tbody = this._getMainElementMarkup();
const areaName = this._getAreaName();
const columnIndexOffset = this._getColumnIndexOffset();
const rowIndexOffset = this._getRowIndexOffset();
const columnIndexMap = areaName === 'column'
? this._createColumnIndexMap(data)
: null;

for (let i = 0; i < rowsCount; i += 1) {
const row = data[i];
Expand All @@ -163,6 +213,7 @@ abstract class AreaItem {
this._getRowClassNames(i, cell, rowClassNames);

let cellText = '';
const span = domAdapter.createElement('span');

if (cell) {
cell.rowspan && td.setAttribute('rowspan', cell.rowspan || 1);
Expand Down Expand Up @@ -194,9 +245,9 @@ abstract class AreaItem {
if (isDefined(cell.expanded)) {
const div = domAdapter.createElement('div');
div.classList.add('dx-expand-icon-container');
const span = domAdapter.createElement('span');
span.classList.add(PIVOTGRID_EXPAND_CLASS);
div.appendChild(span);
const expandSpan = domAdapter.createElement('span');
expandSpan.classList.add(PIVOTGRID_EXPAND_CLASS);
div.appendChild(expandSpan);
td.appendChild(div);
const ariaLabel = String(cell.text ?? cell.value ?? '');
div.setAttribute('role', 'button');
Expand All @@ -206,23 +257,30 @@ abstract class AreaItem {
}

cellText = this._getCellText(cell, encodeHtml);
}

const span = domAdapter.createElement('span');

if (isDefined(cell.wordWrapEnabled)) {
span.style.whiteSpace = cell.wordWrapEnabled ? 'normal' : 'nowrap';
if (isDefined(cell.wordWrapEnabled)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved isDefined(cell.wordWrapEnabled) inside if (cell) check because if cell is undefined, we get type error in runtime

span.style.whiteSpace = cell.wordWrapEnabled ? 'normal' : 'nowrap';
}
}

span.innerHTML = cellText;
td.appendChild(span);

if (cell.sorted) {
const span = domAdapter.createElement('span');
span.classList.add('dx-icon-sorted');
td.appendChild(span);
if (cell?.sorted) {
const sortedSpan = domAdapter.createElement('span');
sortedSpan.classList.add('dx-icon-sorted');
td.appendChild(sortedSpan);
}

this._setCellAriaIndexAttributes(td, {
areaName,
rowIndex: i,
columnIndex: j,
columnIndexMap,
columnIndexOffset,
rowIndexOffset,
});

tr.appendChild(td);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,18 @@ class DataController {
return this._columnPageIndex || 0;
}

getColumnIndexOffset() {
return this._columnsScrollController
? this._columnsScrollController.beginPageIndex() * this._columnsScrollController.pageSize()
: 0;
}

getRowIndexOffset() {
return this._rowsScrollController
? this._rowsScrollController.beginPageIndex() * this._rowsScrollController.pageSize()
: 0;
}

getCellsInfo(getAllData) {
const rowsInfo = this.getRowsInfo(getAllData);
const columnsInfo = this.getColumnsInfo(getAllData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,228 @@ QUnit.module('PivotGrid accessibility markup', {
assert.strictEqual(container.getAttribute('tabindex'), null);
});
});

QUnit.test('Column header and data cells have aria-colindex along shared column axis', function(assert) {
if(!windowUtils.hasWindow()) {
assert.expect(0);
return;
}

const pivotGrid = createPivotGrid({
width: 600, height: 400,
dataSource: createExpandableDataSource()
});
this.clock.tick(10);

const $columnArea = pivotGrid.$element().find('.dx-pivotgrid-horizontal-headers');
const $dataArea = pivotGrid.$element().find('.dx-pivotgrid-area-data');

$columnArea.find('td').each((_, cell) => {
assert.ok(parseInt(cell.getAttribute('aria-colindex'), 10) >= 1, 'column header cell has 1-based aria-colindex');
assert.strictEqual(cell.getAttribute('aria-rowindex'), null, 'column header cell has no aria-rowindex');
});

$dataArea.find('td').each((_, cell) => {
assert.ok(parseInt(cell.getAttribute('aria-colindex'), 10) >= 1, 'data cell has 1-based aria-colindex');
});

const $firstDataRowCells = $dataArea.find('tr').first().find('td');

$firstDataRowCells.each((_, dataCell) => {
const colIndex = dataCell.getAttribute('aria-colindex');
const $headerCells = $columnArea.find(`td[aria-colindex="${colIndex}"]`);

assert.ok($headerCells.length > 0, `column header exists for aria-colindex ${colIndex}`);
});
});

QUnit.test('Row header and data cells have aria-rowindex along shared row axis', function(assert) {
if(!windowUtils.hasWindow()) {
assert.expect(0);
return;
}

const pivotGrid = createPivotGrid({
width: 600, height: 400,
dataSource: createExpandableDataSource()
});
this.clock.tick(10);

const $rowArea = pivotGrid.$element().find('.dx-pivotgrid-vertical-headers');
const $dataArea = pivotGrid.$element().find('.dx-pivotgrid-area-data');

$rowArea.find('td').each((_, cell) => {
assert.ok(parseInt(cell.getAttribute('aria-rowindex'), 10) >= 1, 'row header cell has 1-based aria-rowindex');
assert.strictEqual(cell.getAttribute('aria-colindex'), null, 'row header cell has no aria-colindex');
});

$dataArea.find('tr').each((rowIndex, row) => {
const expectedRowIndex = String(rowIndex + 1);

$(row).find('td').each((_, cell) => {
assert.strictEqual(cell.getAttribute('aria-rowindex'), expectedRowIndex, 'data cell aria-rowindex');
});

const $rowHeaderCells = $rowArea.find('tr').eq(rowIndex).find('td');

$rowHeaderCells.each((_, cell) => {
assert.strictEqual(cell.getAttribute('aria-rowindex'), expectedRowIndex, 'row header aria-rowindex matches data row');
});
});
});

QUnit.test('Multi-level column header colspan is reflected in aria-colindex', function(assert) {
if(!windowUtils.hasWindow()) {
assert.expect(0);
return;
}

const pivotGrid = createPivotGrid({
width: 600, height: 400,
dataSource: createExpandableDataSource()
});
this.clock.tick(10);

const $columnArea = pivotGrid.$element().find('.dx-pivotgrid-horizontal-headers');
const $topRowCells = $columnArea.find('tr').first().find('td');

assert.ok($topRowCells.length > 0, 'top header row has cells');

const $leafRow = $columnArea.find('tr').last();

$topRowCells.each((_, cell) => {
const colspan = cell.colSpan || 1;
const colIndex = parseInt(cell.getAttribute('aria-colindex'), 10);

assert.ok(colIndex >= 1, 'aria-colindex is 1-based');

if(colspan > 1) {
const leafColIndices = $leafRow.find('td').toArray()
.map(c => parseInt(c.getAttribute('aria-colindex'), 10))
.filter(ci => ci >= colIndex && ci < colIndex + colspan);

assert.strictEqual(leafColIndices.length, colspan,
`parent with colspan=${colspan} at colindex=${colIndex} has exactly ${colspan} leaf columns`);
assert.strictEqual(leafColIndices[0], colIndex,
'first leaf aria-colindex matches parent');
assert.strictEqual(leafColIndices[leafColIndices.length - 1], colIndex + colspan - 1,
'last leaf aria-colindex equals parent colindex + colspan - 1');
}
});
});

QUnit.test('aria-colindex and aria-rowindex account for virtual scrolling offsets', function(assert) {
if(!windowUtils.hasWindow()) {
assert.expect(0);
return;
}

const store = [];
for(let i = 0; i < 50; i++) {
store.push({ row: `row${i}`, column: `col${i}`, value: i + 1 });
}

const pivotGrid = createPivotGrid({
width: 120,
height: 120,
fieldChooser: { enabled: false },
scrolling: {
mode: 'virtual',
timeout: 0,
virtualColumnWidth: 20,
virtualRowHeight: 20
},
dataSource: {
fields: [
{ dataField: 'row', area: 'row' },
{ dataField: 'column', area: 'column' },
{ dataField: 'value', area: 'data' }
],
store
}
});
this.clock.tick(10);

const dataController = pivotGrid._dataController;
const columnPageSize = dataController._columnsScrollController.pageSize();
const rowPageSize = dataController._rowsScrollController.pageSize();

const assertAriaIndices = (columnOffset, rowOffset, messagePrefix) => {
const $dataRows = pivotGrid.$element().find('.dx-pivotgrid-area-data tr');
const $firstDataCell = $dataRows.first().find('td').first();
const $lastDataCell = $dataRows.last().find('td').last();
const $firstColumnHeaderCell = pivotGrid.$element().find('.dx-pivotgrid-horizontal-headers td[aria-colindex]').first();
const $lastColumnHeaderCell = pivotGrid.$element().find('.dx-pivotgrid-horizontal-headers tr').last().find('td[aria-colindex]').last();
const $firstRowHeaderCell = pivotGrid.$element().find('.dx-pivotgrid-vertical-headers tr').first().find('td').first();
const $lastRowHeaderCell = pivotGrid.$element().find('.dx-pivotgrid-vertical-headers tr').last().find('td').last();

assert.strictEqual(
dataController.getColumnIndexOffset(),
columnOffset,
`${messagePrefix}: getColumnIndexOffset`
);
assert.strictEqual(
dataController.getRowIndexOffset(),
rowOffset,
`${messagePrefix}: getRowIndexOffset`
);
assert.strictEqual(
$firstDataCell.attr('aria-colindex'),
String(columnOffset + 1),
`${messagePrefix}: first data cell aria-colindex`
);
assert.strictEqual(
$firstColumnHeaderCell.attr('aria-colindex'),
String(columnOffset + 1),
`${messagePrefix}: first column header aria-colindex`
);
assert.ok(
parseInt($lastColumnHeaderCell.attr('aria-colindex'), 10) > columnOffset + 1,
`${messagePrefix}: last column header aria-colindex is greater than first`
);
assert.strictEqual(
$firstDataCell.attr('aria-rowindex'),
String(rowOffset + 1),
`${messagePrefix}: first data cell aria-rowindex`
);
assert.strictEqual(
$firstRowHeaderCell.attr('aria-rowindex'),
String(rowOffset + 1),
`${messagePrefix}: first row header aria-rowindex`
);
assert.ok(
parseInt($lastDataCell.attr('aria-rowindex'), 10) > rowOffset + 1,
`${messagePrefix}: last data cell aria-rowindex is greater than first`
);
assert.ok(
parseInt($lastRowHeaderCell.attr('aria-rowindex'), 10) > rowOffset + 1,
`${messagePrefix}: last row header aria-rowindex is greater than first`
);
};

assertAriaIndices(0, 0, 'initial');

const columnBeginPageIndex = 2;
const rowBeginPageIndex = 1;

const stubs = [
sinon.stub(dataController._columnsScrollController, 'beginPageIndex').returns(columnBeginPageIndex),
sinon.stub(dataController._rowsScrollController, 'beginPageIndex').returns(rowBeginPageIndex),
sinon.stub(dataController._columnsScrollController, 'endPageIndex').returns(columnBeginPageIndex),
sinon.stub(dataController._rowsScrollController, 'endPageIndex').returns(rowBeginPageIndex),
];

try {
dataController.changed.fire();
this.clock.tick(10);

assertAriaIndices(
columnBeginPageIndex * columnPageSize,
rowBeginPageIndex * rowPageSize,
'after virtual page change'
);
} finally {
stubs.forEach(s => s.restore());
}
});
});
Loading