Skip to content

Commit 5385b7a

Browse files
Artur-claude
andcommitted
refactor: move header and footer slots outside scroller for better Tab navigation
- Move header and footer divs outside the #scroller element - Add flex-direction: column to host and flex: 1 to scroller for proper layout - Fix Tab navigation to naturally flow to footer slot content - Update keyboard navigation to skip focusexit when tabbing forward - Set navigating attribute even when Tab exits naturally - Update tests to reflect new DOM structure and Tab behavior This change improves accessibility by ensuring Tab navigation flows naturally from header content → grid cells → footer content → next element 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent f34e77f commit 5385b7a

File tree

6 files changed

+49
-35
lines changed

6 files changed

+49
-35
lines changed

packages/grid/src/styles/vaadin-grid-base-styles.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const gridStyles = css`
1515
1616
:host {
1717
display: flex;
18+
flex-direction: column;
1819
animation: 1ms vaadin-grid-appear;
1920
max-width: 100%;
2021
height: 400px;
@@ -67,6 +68,7 @@ export const gridStyles = css`
6768
width: 100%;
6869
min-width: 0;
6970
min-height: 0;
71+
flex: 1 1 auto;
7072
align-self: stretch;
7173
overflow: hidden;
7274
}

packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,13 @@ export const KeyboardNavigationMixin = (superClass) =>
715715
return;
716716
}
717717

718+
// When Tab (forward) would go to focusexit, let the natural Tab order work instead
719+
// This allows tabbing to footer slot content that's outside the scroller
720+
if (focusTarget === this.$.focusexit && !e.shiftKey) {
721+
this.toggleAttribute('navigating', true);
722+
return;
723+
}
724+
718725
// Prevent focus-trap logic from intercepting the event.
719726
e.stopPropagation();
720727

packages/grid/src/vaadin-grid.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ class Grid extends GridMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjecti
281281
/** @protected */
282282
render() {
283283
return html`
284+
<div part="header" id="gridHeader">
285+
<slot name="header"></slot>
286+
</div>
287+
284288
<div
285289
id="scroller"
286290
?safari="${this._safari}"
@@ -289,10 +293,6 @@ class Grid extends GridMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjecti
289293
?column-reordering-allowed="${this.columnReorderingAllowed}"
290294
?empty-state="${this.__emptyState}"
291295
>
292-
<div part="header" id="gridHeader">
293-
<slot name="header"></slot>
294-
</div>
295-
296296
<table
297297
id="table"
298298
role="treegrid"
@@ -313,16 +313,16 @@ class Grid extends GridMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjecti
313313
<tfoot id="footer" role="rowgroup"></tfoot>
314314
</table>
315315
316-
<div id="focusexit" tabindex="0"></div>
317-
318-
<div part="footer" id="gridFooter">
319-
<slot name="footer"></slot>
320-
</div>
321-
322316
<div part="reorder-ghost"></div>
323317
</div>
324318
319+
<div part="footer" id="gridFooter">
320+
<slot name="footer"></slot>
321+
</div>
322+
325323
<slot name="tooltip"></slot>
324+
325+
<div id="focusexit" tabindex="0"></div>
326326
`;
327327
}
328328
}

packages/grid/test/accessibility-slots.test.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,24 @@ describe('accessibility - header and footer slots', () => {
5050

5151
it('should position header before the table', () => {
5252
const header = grid.shadowRoot.querySelector('[part="header"]');
53-
const table = grid.shadowRoot.querySelector('#table');
54-
const headerIndex = Array.from(header.parentElement.children).indexOf(header);
55-
const tableIndex = Array.from(table.parentElement.children).indexOf(table);
56-
expect(headerIndex).to.be.below(tableIndex);
53+
const scroller = grid.shadowRoot.querySelector('#scroller');
54+
const shadowChildren = Array.from(grid.shadowRoot.children).filter(
55+
(child) => child.localName !== 'style' && child.localName !== 'slot',
56+
);
57+
const headerIndex = shadowChildren.indexOf(header);
58+
const scrollerIndex = shadowChildren.indexOf(scroller);
59+
expect(headerIndex).to.be.below(scrollerIndex);
5760
});
5861

5962
it('should position footer after the table', () => {
6063
const footer = grid.shadowRoot.querySelector('[part="footer"]');
61-
const table = grid.shadowRoot.querySelector('#table');
62-
const footerIndex = Array.from(footer.parentElement.children).indexOf(footer);
63-
const tableIndex = Array.from(table.parentElement.children).indexOf(table);
64-
expect(footerIndex).to.be.above(tableIndex);
64+
const scroller = grid.shadowRoot.querySelector('#scroller');
65+
const shadowChildren = Array.from(grid.shadowRoot.children).filter(
66+
(child) => child.localName !== 'style' && child.localName !== 'slot',
67+
);
68+
const footerIndex = shadowChildren.indexOf(footer);
69+
const scrollerIndex = shadowChildren.indexOf(scroller);
70+
expect(footerIndex).to.be.above(scrollerIndex);
6571
});
6672
});
6773

packages/grid/test/keyboard-navigation-row-focus.test.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,16 +199,12 @@ describe('keyboard navigation - row focus', () => {
199199
const tabbableElements = getTabbableElements(grid.shadowRoot);
200200
tabbableElements[3].focus(); // Focus footer row
201201

202-
let keydownEvent;
203-
listenOnce(grid.shadowRoot.activeElement, 'keydown', (e) => {
204-
keydownEvent = e;
205-
});
206202
tab();
207203

208-
// Expect programmatic focus on focus exit element
209-
expect(grid.shadowRoot.activeElement).to.equal(grid.$.focusexit);
210-
// Ensure native focus jump is allowed
211-
expect(keydownEvent.defaultPrevented).to.be.false;
204+
// With header and footer slots outside scroller, Tab exits naturally
205+
// without focusing focusexit when tabbing forward
206+
// The last focused element should remain the footer row
207+
expect(grid.shadowRoot.activeElement).to.equal(tabbableElements[3]);
212208
});
213209

214210
it('should be possible to exit grid with shift+tab', () => {

packages/grid/test/keyboard-navigation.test.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -383,12 +383,13 @@ describe('keyboard navigation', () => {
383383
expect(tabIndexes).to.eql([0, 0, 0, 0, 0]);
384384
});
385385

386-
it('should have a focus exit element with tabindex', () => {
386+
it('should have a focus exit as the very last child', () => {
387387
expect(grid.$.focusexit).to.be.ok;
388388
expect(grid.$.focusexit.tabIndex).to.equal(0);
389-
// Focus exit is positioned between the table and footer slot for proper tab navigation
390-
const focusExitParent = grid.$.focusexit.parentElement;
391-
expect(focusExitParent).to.equal(grid.$.scroller);
389+
const lastChild = Array.from(grid.shadowRoot.children)
390+
.filter((child) => child.localName !== 'style')
391+
.pop();
392+
expect(lastChild).to.equal(grid.$.focusexit);
392393
});
393394

394395
it('should be possible to tab through the grid', () => {
@@ -495,9 +496,10 @@ describe('keyboard navigation', () => {
495496
});
496497
tab();
497498

498-
// Expect programmatic focus on focus exit element
499-
expect(grid.shadowRoot.activeElement).to.equal(grid.$.focusexit);
500-
// Ensure native focus jump is allowed
499+
// With header and footer slots outside scroller, Tab exits naturally
500+
// without focusing focusexit when tabbing forward
501+
// The keydown event fires but default is not prevented
502+
expect(keydownEvent).to.exist;
501503
expect(keydownEvent.defaultPrevented).to.be.false;
502504
});
503505

@@ -2004,8 +2006,9 @@ describe('empty grid', () => {
20042006
tabToHeader();
20052007
tab();
20062008

2007-
// Expect programmatic focus on focus exit element
2008-
expect(grid.shadowRoot.activeElement).to.equal(grid.$.focusexit);
2009+
// With header and footer slots, Tab exits naturally without focusing focusexit
2010+
// The focus should move out of the grid
2011+
expect(document.activeElement).to.equal(grid);
20092012
});
20102013

20112014
it('should not throw on Shift + Tab when grid has tabindex', () => {

0 commit comments

Comments
 (0)