-
Notifications
You must be signed in to change notification settings - Fork 671
PivotGrid - A11y: describe field areas, open field context menu from keyboard, axe tests #34375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
aleksei-semikozov
merged 22 commits into
DevExpress:main
from
aleksei-semikozov:pivotgrid-a11y-4196-polish
Jul 28, 2026
Merged
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
3025411
PivotGrid - KBN - Add ARIA attributes to area fields
eaa4083
PivotGrid - KBN - Fix ascending sort ARIA field message key typo
ffafcff
Merge branch 'main' into pivotgrid-kbn-fields-aria-attrs-main
745ab50
PivotGrid - KBN - Reset stale menubar role on hidden field areas
683a793
Merge remote-tracking branch 'upstream/main' into pivotgrid-a11y-4196…
aleksei-semikozov 81b084a
PivotGrid - Add axe accessibility tests
827e285
PivotGrid - Describe field area keyboard interactions via aria-descri…
089cf97
PivotGrid - Open field context menu from keyboard
982706d
PivotGrid - Handle the context menu handler option change and dispatc…
1a46891
PivotGrid - Describe data cells with their row and column headers
c201add
PivotGrid - Resolve area sections defensively for partially stubbed a…
e32ae6d
PivotGrid - Revert data-cell aria-describedby; deliver field-area key…
3ec81ca
Merge branch 'main' into pivotgrid-a11y-4196-polish
06e0710
PivotGrid - Stabilize axe a11y tests and guard empty field-area caption
1835da0
PivotGrid - Disable best-practice region rule for overlay axe scenarios
f312718
PivotGrid - Trim self-evident comments and address review nits (preve…
898b050
PivotGrid - Stop announcing expandable header cell captions twice
edd81fc
PivotGrid - Reserve aria-colindex columns for row headers so they are…
ff497a7
PivotGrid - Drop explanatory comments from a11y source changes
b9ef650
PivotGrid - Announce field hotkeys once on the component instead of p…
d80a192
PivotGrid - Derive reserved row-header width from the model and guard…
4b5d464
PivotGrid - Rename the field hotkeys aria message to dxPivotGrid-aria…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
106 changes: 106 additions & 0 deletions
106
e2e/testcafe-devextreme/tests/accessibility/pivotGrid.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import PivotGrid from 'devextreme-testcafe-models/pivotGrid'; | ||
| import { Selector } from 'testcafe'; | ||
| import url from '../../helpers/getPageUrl'; | ||
| import { createWidget } from '../../helpers/createWidget'; | ||
| import { a11yCheck } from '../../helpers/accessibility/utils'; | ||
|
|
||
| fixture.disablePageReloads`PivotGrid - Accessibility` | ||
| .page(url(__dirname, '../container.html')); | ||
|
|
||
| const PIVOT_GRID_SELECTOR = '#container'; | ||
|
|
||
| const createConfig = () => ({ | ||
| width: 1000, | ||
| allowSorting: true, | ||
| allowSortingBySummary: true, | ||
| allowFiltering: true, | ||
| allowExpandAll: true, | ||
| showBorders: true, | ||
| fieldPanel: { | ||
| visible: true, | ||
| }, | ||
| fieldChooser: { | ||
| enabled: true, | ||
| height: 500, | ||
| }, | ||
| dataSource: { | ||
| fields: [{ | ||
| dataField: 'country', | ||
| area: 'filter', | ||
| }, { | ||
| caption: 'Region', | ||
| width: 120, | ||
| dataField: 'region', | ||
| area: 'row', | ||
| }, { | ||
| caption: 'City', | ||
| dataField: 'city', | ||
| width: 150, | ||
| area: 'row', | ||
| }, { | ||
| caption: 'Country', | ||
| dataField: 'country', | ||
| area: 'column', | ||
| }, { | ||
| dataField: 'date', | ||
| dataType: 'date', | ||
| area: 'column', | ||
| }, { | ||
| groupName: 'date', | ||
| groupInterval: 'year', | ||
| expanded: true, | ||
| area: 'column', | ||
| }, { | ||
| caption: 'Sales', | ||
| dataField: 'amount', | ||
| dataType: 'number', | ||
| summaryType: 'sum', | ||
| area: 'data', | ||
| }], | ||
| store: [{ | ||
| region: 'Africa', | ||
| country: 'Egypt', | ||
| city: 'Cairo', | ||
| amount: 500, | ||
| date: new Date('2015-05-26'), | ||
| }, { | ||
| region: 'South America', | ||
| country: 'Argentina', | ||
| city: 'Buenos Aires', | ||
| amount: 780, | ||
| date: new Date('2015-05-07'), | ||
| }], | ||
| }, | ||
| }); | ||
|
|
||
| test('grid with field panel', async (t) => { | ||
| await a11yCheck(t, {}, PIVOT_GRID_SELECTOR); | ||
| }).before(async () => createWidget('dxPivotGrid', createConfig())); | ||
|
|
||
| test('field chooser popup', async (t) => { | ||
| const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); | ||
|
|
||
| await t.click(pivotGrid.getFieldChooserButton()); | ||
|
|
||
| await a11yCheck(t); | ||
| }).before(async () => createWidget('dxPivotGrid', createConfig())); | ||
|
|
||
| test('header filter popup', async (t) => { | ||
| const pivotGrid = new PivotGrid(PIVOT_GRID_SELECTOR); | ||
| const filterIcon = pivotGrid | ||
| .getColumnHeaderArea() | ||
| .getHeaderFilterIcon() | ||
| .element; | ||
|
|
||
| await t.click(filterIcon); | ||
|
|
||
| await a11yCheck(t); | ||
| }).before(async () => createWidget('dxPivotGrid', createConfig())); | ||
|
|
||
| test('cell context menu', async (t) => { | ||
| const columnHeaderCell = Selector('.dx-pivotgrid-horizontal-headers td[tabindex]'); | ||
|
|
||
| await t.rightClick(columnHeaderCell); | ||
|
|
||
| await a11yCheck(t); | ||
| }).before(async () => createWidget('dxPivotGrid', createConfig())); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
packages/devextreme/js/__internal/grids/pivot_grid/a11y/data_cell_description.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { describe, expect, it } from '@jest/globals'; | ||
|
|
||
| import { describeDataCellsWithHeaders } from './data_cell_description'; | ||
|
|
||
| const createSection = (tag: 'thead' | 'tbody', rowsHtml: string): HTMLTableSectionElement => { | ||
| const table = document.createElement('table'); | ||
| table.innerHTML = `<${tag}>${rowsHtml}</${tag}>`; | ||
| return table.querySelector(tag) as HTMLTableSectionElement; | ||
| }; | ||
|
aleksei-semikozov marked this conversation as resolved.
Outdated
|
||
|
|
||
| const getDescribingCells = (cell: HTMLTableCellElement | null): (Element | null)[] => (cell?.getAttribute('aria-describedby') ?? '') | ||
| .split(' ') | ||
| .filter((id) => !!id) | ||
| .map((id) => document.getElementById(id)); | ||
|
|
||
| describe('describeDataCellsWithHeaders', () => { | ||
| it('should reference the column header path including colspan groups', () => { | ||
| const columns = createSection('thead', ` | ||
| <tr><td id="group" colspan="2" aria-colindex="1">2015</td><td id="total" rowspan="2" aria-colindex="3">Grand Total</td></tr> | ||
| <tr><td id="q1" aria-colindex="1">Q1</td><td id="q2" aria-colindex="2">Q2</td></tr> | ||
| `); | ||
| const data = createSection('tbody', ` | ||
| <tr><td aria-colindex="1" aria-rowindex="1">10</td><td aria-colindex="2" aria-rowindex="1">20</td><td aria-colindex="3" aria-rowindex="1">30</td></tr> | ||
| `); | ||
| document.body.append(columns, data); | ||
|
|
||
| describeDataCellsWithHeaders(columns, undefined, data); | ||
|
|
||
| const cells = data.querySelectorAll('td'); | ||
| expect(getDescribingCells(cells[0])).toEqual([ | ||
| document.getElementById('group'), | ||
| document.getElementById('q1'), | ||
| ]); | ||
| expect(getDescribingCells(cells[1])).toEqual([ | ||
| document.getElementById('group'), | ||
| document.getElementById('q2'), | ||
| ]); | ||
| expect(getDescribingCells(cells[2])).toEqual([ | ||
| document.getElementById('total'), | ||
| ]); | ||
| }); | ||
|
|
||
| it('should reference the row header path including rowspan groups', () => { | ||
| const rows = createSection('tbody', ` | ||
| <tr><td id="africa" rowspan="2" aria-rowindex="1">Africa</td><td id="cairo" aria-rowindex="1">Cairo</td></tr> | ||
| <tr><td id="luxor" aria-rowindex="2">Luxor</td></tr> | ||
| `); | ||
| const data = createSection('tbody', ` | ||
| <tr><td aria-colindex="1" aria-rowindex="1">10</td></tr> | ||
| <tr><td aria-colindex="1" aria-rowindex="2">20</td></tr> | ||
| `); | ||
| document.body.append(rows, data); | ||
|
|
||
| describeDataCellsWithHeaders(undefined, rows, data); | ||
|
|
||
| const cells = data.querySelectorAll('td'); | ||
| expect(getDescribingCells(cells[0])).toEqual([ | ||
| document.getElementById('africa'), | ||
| document.getElementById('cairo'), | ||
| ]); | ||
| expect(getDescribingCells(cells[1])).toEqual([ | ||
| document.getElementById('africa'), | ||
| document.getElementById('luxor'), | ||
| ]); | ||
| }); | ||
|
|
||
| it('should combine row and column paths in the row-then-column order', () => { | ||
| const columns = createSection('thead', '<tr><td id="c" aria-colindex="1">2015</td></tr>'); | ||
| const rows = createSection('tbody', '<tr><td id="r" aria-rowindex="1">Africa</td></tr>'); | ||
| const data = createSection('tbody', '<tr><td aria-colindex="1" aria-rowindex="1">10</td></tr>'); | ||
| document.body.append(columns, rows, data); | ||
|
|
||
| describeDataCellsWithHeaders(columns, rows, data); | ||
|
|
||
| expect(getDescribingCells(data.querySelector('td'))).toEqual([ | ||
| document.getElementById('r'), | ||
| document.getElementById('c'), | ||
| ]); | ||
| }); | ||
|
|
||
| it('should respect the virtual scrolling index offset', () => { | ||
| const columns = createSection('thead', '<tr><td id="c5" aria-colindex="5">2015</td><td id="c6" aria-colindex="6">2016</td></tr>'); | ||
| const data = createSection('tbody', '<tr><td aria-colindex="5" aria-rowindex="1">10</td><td aria-colindex="6" aria-rowindex="1">20</td></tr>'); | ||
| document.body.append(columns, data); | ||
|
|
||
| describeDataCellsWithHeaders(columns, undefined, data); | ||
|
|
||
| const cells = data.querySelectorAll('td'); | ||
| expect(getDescribingCells(cells[0])).toEqual([document.getElementById('c5')]); | ||
| expect(getDescribingCells(cells[1])).toEqual([document.getElementById('c6')]); | ||
| }); | ||
|
|
||
| it('should reuse existing header cell ids and keep generated ones stable across runs', () => { | ||
| const columns = createSection('thead', '<tr><td id="existing" aria-colindex="1">2015</td><td aria-colindex="2">2016</td></tr>'); | ||
| const data = createSection('tbody', '<tr><td aria-colindex="1" aria-rowindex="1">10</td><td aria-colindex="2" aria-rowindex="1">20</td></tr>'); | ||
| document.body.append(columns, data); | ||
|
|
||
| describeDataCellsWithHeaders(columns, undefined, data); | ||
| const generatedId = (columns.querySelectorAll('td')[1]).id; | ||
| describeDataCellsWithHeaders(columns, undefined, data); | ||
|
|
||
| expect((columns.querySelectorAll('td')[0]).id).toBe('existing'); | ||
| expect((columns.querySelectorAll('td')[1]).id).toBe(generatedId); | ||
| expect(generatedId).toMatch(/^dx-/); | ||
| }); | ||
|
|
||
| it('should remove a stale description when no header chains match', () => { | ||
| const data = createSection('tbody', '<tr><td aria-colindex="1" aria-rowindex="1" aria-describedby="stale">10</td></tr>'); | ||
| document.body.append(data); | ||
|
|
||
| describeDataCellsWithHeaders(undefined, undefined, data); | ||
|
|
||
| expect(data.querySelector('td')?.hasAttribute('aria-describedby')).toBe(false); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.