Skip to content

Commit 347667b

Browse files
Artur-claude
andcommitted
test: add accessibility tests for grid header and footer slots
Added comprehensive accessibility tests to ensure the header and footer slots maintain proper keyboard navigation, ARIA relationships, and screen reader support without interfering with the grid's existing accessibility features. Part of #986 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 04d5f94 commit 347667b

File tree

1 file changed

+176
-0
lines changed

1 file changed

+176
-0
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { expect } from '@vaadin/chai-plugins';
2+
import { fixtureSync, nextFrame } from '@vaadin/testing-helpers';
3+
import '../src/vaadin-grid.js';
4+
import '../src/vaadin-grid-column.js';
5+
6+
describe('accessibility - header and footer slots', () => {
7+
let grid;
8+
9+
beforeEach(async () => {
10+
grid = fixtureSync(`
11+
<vaadin-grid>
12+
<div slot="header" id="gridToolbar">
13+
<button id="addBtn">Add Item</button>
14+
<input type="search" placeholder="Search..." id="searchInput">
15+
</div>
16+
17+
<vaadin-grid-column path="name" header="Name"></vaadin-grid-column>
18+
<vaadin-grid-column path="email" header="Email"></vaadin-grid-column>
19+
20+
<div slot="footer" id="gridStatus">
21+
<span>Total: 5 items</span>
22+
<button id="nextBtn">Next Page</button>
23+
</div>
24+
</vaadin-grid>
25+
`);
26+
27+
grid.items = [
28+
{ name: 'John', email: '[email protected]' },
29+
{ name: 'Jane', email: '[email protected]' },
30+
];
31+
32+
await nextFrame();
33+
});
34+
35+
describe('DOM structure', () => {
36+
it('should have header slot content in the shadow DOM', () => {
37+
const headerSlot = grid.shadowRoot.querySelector('slot[name="header"]');
38+
const assignedNodes = headerSlot.assignedNodes();
39+
expect(assignedNodes).to.have.length(1);
40+
expect(assignedNodes[0].id).to.equal('gridToolbar');
41+
});
42+
43+
it('should have footer slot content in the shadow DOM', () => {
44+
const footerSlot = grid.shadowRoot.querySelector('slot[name="footer"]');
45+
const assignedNodes = footerSlot.assignedNodes();
46+
expect(assignedNodes).to.have.length(1);
47+
expect(assignedNodes[0].id).to.equal('gridStatus');
48+
});
49+
50+
it('should position header before the table', () => {
51+
const header = grid.shadowRoot.querySelector('[part="header"]');
52+
const table = grid.shadowRoot.querySelector('#table');
53+
const headerIndex = Array.from(header.parentElement.children).indexOf(header);
54+
const tableIndex = Array.from(table.parentElement.children).indexOf(table);
55+
expect(headerIndex).to.be.below(tableIndex);
56+
});
57+
58+
it('should position footer after the table', () => {
59+
const footer = grid.shadowRoot.querySelector('[part="footer"]');
60+
const table = grid.shadowRoot.querySelector('#table');
61+
const footerIndex = Array.from(footer.parentElement.children).indexOf(footer);
62+
const tableIndex = Array.from(table.parentElement.children).indexOf(table);
63+
expect(footerIndex).to.be.above(tableIndex);
64+
});
65+
});
66+
67+
describe('keyboard navigation', () => {
68+
it('should allow keyboard navigation to header slot content', () => {
69+
const addButton = grid.querySelector('#addBtn');
70+
addButton.focus();
71+
expect(document.activeElement).to.equal(addButton);
72+
});
73+
74+
it('should allow keyboard navigation to footer slot content', () => {
75+
const nextButton = grid.querySelector('#nextBtn');
76+
nextButton.focus();
77+
expect(document.activeElement).to.equal(nextButton);
78+
});
79+
80+
it('should maintain grid table keyboard navigation', () => {
81+
const table = grid.shadowRoot.querySelector('#table');
82+
table.focus();
83+
// When the table is focused, the actual focus goes to a cell
84+
const activeElement = grid.shadowRoot.activeElement;
85+
expect(activeElement).to.exist;
86+
expect(activeElement.getAttribute('role')).to.be.oneOf(['columnheader', 'gridcell']);
87+
});
88+
});
89+
90+
describe('ARIA relationships', () => {
91+
it('should not interfere with grid ARIA attributes', () => {
92+
const table = grid.shadowRoot.querySelector('#table');
93+
expect(table.getAttribute('role')).to.equal('treegrid');
94+
expect(table.getAttribute('aria-multiselectable')).to.equal('true');
95+
});
96+
97+
it('should not add inappropriate ARIA roles to header/footer', () => {
98+
const header = grid.shadowRoot.querySelector('[part="header"]');
99+
const footer = grid.shadowRoot.querySelector('[part="footer"]');
100+
101+
// Header and footer should not have roles that conflict with their content
102+
expect(header.getAttribute('role')).to.be.null;
103+
expect(footer.getAttribute('role')).to.be.null;
104+
});
105+
106+
it('should allow custom ARIA attributes on slotted content', () => {
107+
const toolbar = grid.querySelector('#gridToolbar');
108+
toolbar.setAttribute('role', 'toolbar');
109+
toolbar.setAttribute('aria-label', 'Grid actions');
110+
111+
expect(toolbar.getAttribute('role')).to.equal('toolbar');
112+
expect(toolbar.getAttribute('aria-label')).to.equal('Grid actions');
113+
});
114+
});
115+
116+
describe('screen reader announcement', () => {
117+
it('should allow header content to be announced independently', () => {
118+
const searchInput = grid.querySelector('#searchInput');
119+
expect(searchInput.getAttribute('placeholder')).to.equal('Search...');
120+
121+
// Screen readers should be able to announce this input field
122+
expect(searchInput.tagName.toLowerCase()).to.equal('input');
123+
expect(searchInput.type).to.equal('search');
124+
});
125+
126+
it('should allow footer content to be announced independently', () => {
127+
const footerText = grid.querySelector('#gridStatus').textContent;
128+
expect(footerText).to.include('Total: 5 items');
129+
});
130+
131+
it('should preserve grid accessible name', async () => {
132+
grid.accessibleName = 'User List';
133+
await nextFrame();
134+
const table = grid.shadowRoot.querySelector('#table');
135+
expect(table.getAttribute('aria-label')).to.equal('User List');
136+
});
137+
});
138+
139+
describe('focus management', () => {
140+
it('should not trap focus in header', () => {
141+
const addButton = grid.querySelector('#addBtn');
142+
const searchInput = grid.querySelector('#searchInput');
143+
144+
addButton.focus();
145+
expect(document.activeElement).to.equal(addButton);
146+
147+
searchInput.focus();
148+
expect(document.activeElement).to.equal(searchInput);
149+
});
150+
151+
it('should not trap focus in footer', () => {
152+
const nextButton = grid.querySelector('#nextBtn');
153+
nextButton.focus();
154+
expect(document.activeElement).to.equal(nextButton);
155+
});
156+
157+
it('should maintain proper focus order', () => {
158+
// Focus should flow naturally through header -> grid -> footer
159+
const focusableElements = [
160+
grid.querySelector('#addBtn'),
161+
grid.querySelector('#searchInput'),
162+
grid.shadowRoot.querySelector('#table'),
163+
grid.querySelector('#nextBtn'),
164+
];
165+
166+
// Verify all elements are focusable
167+
focusableElements.forEach((el) => {
168+
if (el === grid.shadowRoot.querySelector('#table')) {
169+
expect(el.getAttribute('tabindex')).to.equal('0');
170+
} else {
171+
expect(el.tabIndex).to.be.at.least(0);
172+
}
173+
});
174+
});
175+
});
176+
});

0 commit comments

Comments
 (0)