Skip to content

Commit b29b255

Browse files
fix: Ensure focus is returned to active tab (#3266)
1 parent 35a2b2a commit b29b255

File tree

2 files changed

+125
-72
lines changed

2 files changed

+125
-72
lines changed

src/tabs/__integ__/tabs.test.ts

Lines changed: 124 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,13 @@ class TabsPage extends BasePageObject {
6666
}
6767
}
6868

69-
const setupTest = (testFn: (page: TabsPage) => Promise<void>, smallViewport = false) => {
69+
const setupTest = (
70+
testFn: (page: TabsPage) => Promise<void>,
71+
{ smallViewport = false, pagePath = 'responsive-integ' }: { smallViewport?: boolean; pagePath?: string } = {}
72+
) => {
7073
return useBrowser(async browser => {
7174
const page = new TabsPage(browser);
72-
await browser.url('#/light/tabs/responsive-integ');
75+
await browser.url(`#/light/tabs/${pagePath}`);
7376
await page.waitForVisible(wrapper.findTabContent().toSelector());
7477
if (smallViewport) {
7578
await page.setWindowSize({ width: 400, height: 1000 });
@@ -80,16 +83,22 @@ const setupTest = (testFn: (page: TabsPage) => Promise<void>, smallViewport = fa
8083

8184
test(
8285
'displays pagination buttons in small viewports',
83-
setupTest(async page => {
84-
await page.hasPaginationButtons(true);
85-
}, true)
86+
setupTest(
87+
async page => {
88+
await page.hasPaginationButtons(true);
89+
},
90+
{ smallViewport: true }
91+
)
8692
);
8793

8894
test(
8995
'does not display pagination buttons in large viewports',
90-
setupTest(async page => {
91-
await page.hasPaginationButtons(false);
92-
}, false)
96+
setupTest(
97+
async page => {
98+
await page.hasPaginationButtons(false);
99+
},
100+
{ smallViewport: false }
101+
)
93102
);
94103

95104
test(
@@ -127,11 +136,14 @@ test(
127136

128137
test(
129138
'does not scroll when using arrows',
130-
setupTest(async page => {
131-
await page.focusTabHeader();
132-
await page.navigateTabList(1);
133-
await expect(page.getScrollLeft()).resolves.toBe(0);
134-
}, true)
139+
setupTest(
140+
async page => {
141+
await page.focusTabHeader();
142+
await page.navigateTabList(1);
143+
await expect(page.getScrollLeft()).resolves.toBe(0);
144+
},
145+
{ smallViewport: true }
146+
)
135147
);
136148

137149
test(
@@ -166,17 +178,20 @@ test(
166178

167179
test(
168180
'scrolls when using the left/right pagination buttons with the keyboard',
169-
setupTest(async page => {
170-
await page.setWindowSize({ width: 550, height: 1000 });
171-
await page.focusTabHeader();
172-
// arrows have a focus ring (and a tab stop) even in disabled state
173-
await page.keys(['Tab', 'Tab', 'Space']);
174-
await expect(page.isExisting(page.paginationButton('left', true))).resolves.toBe(true);
175-
await expect(page.isExisting(page.paginationButton('right', true))).resolves.toBe(false);
176-
await page.keys(['Shift', 'Tab', 'Shift', 'Tab', 'Space']);
177-
await expect(page.isExisting(page.paginationButton('left', true))).resolves.toBe(false);
178-
await expect(page.isExisting(page.paginationButton('right', true))).resolves.toBe(true);
179-
}, true)
181+
setupTest(
182+
async page => {
183+
await page.setWindowSize({ width: 550, height: 1000 });
184+
await page.focusTabHeader();
185+
// arrows have a focus ring (and a tab stop) even in disabled state
186+
await page.keys(['Tab', 'Tab', 'Space']);
187+
await expect(page.isExisting(page.paginationButton('left', true))).resolves.toBe(true);
188+
await expect(page.isExisting(page.paginationButton('right', true))).resolves.toBe(false);
189+
await page.keys(['Shift', 'Tab', 'Shift', 'Tab', 'Space']);
190+
await expect(page.isExisting(page.paginationButton('left', true))).resolves.toBe(false);
191+
await expect(page.isExisting(page.paginationButton('right', true))).resolves.toBe(true);
192+
},
193+
{ smallViewport: true }
194+
)
180195
);
181196

182197
test(
@@ -244,70 +259,85 @@ test(
244259

245260
test(
246261
'does not scroll on click if not needed',
247-
setupTest(async page => {
248-
await page.click(wrapper.findTabLinkByIndex(2).toSelector());
249-
await expect(page.getScrollLeft()).resolves.toBe(0);
250-
}, true)
262+
setupTest(
263+
async page => {
264+
await page.click(wrapper.findTabLinkByIndex(2).toSelector());
265+
await expect(page.getScrollLeft()).resolves.toBe(0);
266+
},
267+
{ smallViewport: true }
268+
)
251269
);
252270

253271
test(
254272
'scrolls selected tab header into view when focusing',
255-
setupTest(async page => {
256-
await page.click(page.paginationButton('right', true));
257-
await expect(page.getScrollLeft()).resolves.toBeGreaterThan(200);
258-
await page.click('#before');
259-
await page.keys(['Tab', 'Tab']); // Type Tab twice in order to navigate past the scroll left button
260-
await expect(page.getScrollLeft()).resolves.toBe(0);
261-
}, true)
273+
setupTest(
274+
async page => {
275+
await page.click(page.paginationButton('right', true));
276+
await expect(page.getScrollLeft()).resolves.toBeGreaterThan(200);
277+
await page.click('#before');
278+
await page.keys(['Tab', 'Tab']); // Type Tab twice in order to navigate past the scroll left button
279+
await expect(page.getScrollLeft()).resolves.toBe(0);
280+
},
281+
{ smallViewport: true }
282+
)
262283
);
263284

264285
[false, true].forEach(smallViewport => {
265286
test(
266287
'has the same arrow left/right keys behavior when paginated',
267-
setupTest(async page => {
268-
await page.click('#before');
269-
await expect(page.findActiveTabIndex()).resolves.toBe(0);
270-
if (smallViewport) {
271-
// arrows have a focus ring (and a tab stop) even in disabled state
272-
await page.keys('Tab');
273-
}
274-
await page.keys(['Tab', 'ArrowRight']);
275-
await expect(page.findActiveTabIndex()).resolves.toBe(1);
276-
await page.navigateTabList(7);
277-
await expect(page.findActiveTabIndex()).resolves.toBe(0);
278-
await page.navigateTabList(-3);
279-
await expect(page.findActiveTabIndex()).resolves.toBe(5);
280-
await page.navigateTabList(-1);
281-
await expect(page.findActiveTabIndex()).resolves.toBe(3);
282-
}, smallViewport)
288+
setupTest(
289+
async page => {
290+
await page.click('#before');
291+
await expect(page.findActiveTabIndex()).resolves.toBe(0);
292+
if (smallViewport) {
293+
// arrows have a focus ring (and a tab stop) even in disabled state
294+
await page.keys('Tab');
295+
}
296+
await page.keys(['Tab', 'ArrowRight']);
297+
await expect(page.findActiveTabIndex()).resolves.toBe(1);
298+
await page.navigateTabList(7);
299+
await expect(page.findActiveTabIndex()).resolves.toBe(0);
300+
await page.navigateTabList(-3);
301+
await expect(page.findActiveTabIndex()).resolves.toBe(5);
302+
await page.navigateTabList(-1);
303+
await expect(page.findActiveTabIndex()).resolves.toBe(3);
304+
},
305+
{ smallViewport }
306+
)
283307
);
284308
test(
285309
'has the same arrow home/end keys behavior when paginated',
286-
setupTest(async page => {
287-
await page.focusTabHeader();
288-
if (smallViewport) {
289-
// arrows have a focus ring (and a tab stop) even in disabled state
290-
await page.keys('Tab');
291-
}
292-
await page.keys(['End']);
293-
await page.navigateTabList(-2);
294-
await expect(page.findActiveTabIndex()).resolves.toBe(5);
295-
await page.keys(['Home']);
296-
await expect(page.findActiveTabIndex()).resolves.toBe(0);
297-
}, smallViewport)
310+
setupTest(
311+
async page => {
312+
await page.focusTabHeader();
313+
if (smallViewport) {
314+
// arrows have a focus ring (and a tab stop) even in disabled state
315+
await page.keys('Tab');
316+
}
317+
await page.keys(['End']);
318+
await page.navigateTabList(-2);
319+
await expect(page.findActiveTabIndex()).resolves.toBe(5);
320+
await page.keys(['Home']);
321+
await expect(page.findActiveTabIndex()).resolves.toBe(0);
322+
},
323+
{ smallViewport }
324+
)
298325
);
299326
});
300327

301328
test(
302329
'header buttons do not trigger form submission',
303-
setupTest(async page => {
304-
const urlBefore = await page.getUrl();
305-
await page.click(wrapper.find(page.paginationButton('left')).toSelector());
306-
await page.click(wrapper.find(page.paginationButton('right')).toSelector());
307-
const urlAfter = await page.getUrl();
308-
309-
expect(urlAfter).toEqual(urlBefore);
310-
}, true)
330+
setupTest(
331+
async page => {
332+
const urlBefore = await page.getUrl();
333+
await page.click(wrapper.find(page.paginationButton('left')).toSelector());
334+
await page.click(wrapper.find(page.paginationButton('right')).toSelector());
335+
const urlAfter = await page.getUrl();
336+
337+
expect(urlAfter).toEqual(urlBefore);
338+
},
339+
{ smallViewport: true }
340+
)
311341
);
312342

313343
test(
@@ -406,6 +436,29 @@ test(
406436
})
407437
);
408438

439+
test(
440+
'verifies focus returns to active tab',
441+
setupTest(async page => {
442+
await page.click(wrapper.findTabLinkByIndex(6).toSelector());
443+
await page.keys(['Tab']);
444+
await page.keys(['Shift', 'Tab']);
445+
await expect(page.isFocused(wrapper.findTabLinkByIndex(6).toSelector())).resolves.toBe(true);
446+
})
447+
);
448+
449+
test(
450+
'verifies focus returns to active tab (no actions/dismissible)',
451+
setupTest(
452+
async page => {
453+
await page.click(wrapper.findTabLinkByIndex(2).toSelector());
454+
await page.keys(['Tab']);
455+
await page.keys(['Shift', 'Tab']);
456+
await expect(page.isFocused(wrapper.findTabLinkByIndex(2).toSelector())).resolves.toBe(true);
457+
},
458+
{ pagePath: 'controllability' }
459+
)
460+
);
461+
409462
test(
410463
'verifies focus is trapped when action is open',
411464
setupTest(async page => {

src/tabs/tab-header-bar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import styles from './styles.css.js';
4242
import testUtilStyles from './test-classes/styles.css.js';
4343

4444
const tabSelector = `.${styles['tabs-tab-link']}`;
45-
const focusedTabSelector = `[role="tab"].${styles['tabs-tab-focused']}`;
45+
const focusedTabSelector = `.${styles['tabs-tab-focused']}`;
4646
const focusableTabSelector = `.${styles['tabs-tab-focusable']}`;
4747

4848
function dismissButton({

0 commit comments

Comments
 (0)