Skip to content

Commit 1a6ae5a

Browse files
committed
feat(headless/tabs): handle vertical tabs
1 parent c5621a5 commit 1a6ae5a

File tree

3 files changed

+165
-58
lines changed

3 files changed

+165
-58
lines changed

packages/kit-headless/src/components/tabs/tabs-list.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { component$, Slot } from '@builder.io/qwik';
1+
import { component$, QwikIntrinsicElements, Slot } from '@builder.io/qwik';
22
import { Behavior } from './behavior.type';
33

4-
export interface TabListProps {
4+
export type TabListProps = QwikIntrinsicElements['div'] & {
55
labelledBy?: string;
66
behavior?: Behavior;
7-
class?: string;
8-
}
7+
};
98

109
// List of tabs that can be clicked to show different content.
1110
export const TabList = component$((props: TabListProps) => {

packages/kit-headless/src/components/tabs/tabs.spec.tsx

Lines changed: 153 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,27 @@ interface ThreeTabsCompProps {
88
isMiddleDisabled?: boolean;
99
showDisableButton?: boolean;
1010
disabledIndex?: number;
11+
isVertical?: boolean;
1112
}
1213

1314
const ThreeTabsComponent = component$(
1415
({
1516
isMiddleDisabled = false,
1617
showDisableButton = false,
18+
isVertical = false,
1719
disabledIndex,
1820
}: ThreeTabsCompProps) => {
1921
const isMiddleDisabledSignal = useSignal(isMiddleDisabled);
2022

2123
return (
2224
<>
23-
<Tabs data-testid="tabs">
24-
<TabList>
25+
<Tabs data-testid="tabs" vertical={isVertical}>
26+
<TabList
27+
style={{
28+
display: 'flex',
29+
flexDirection: isVertical ? 'column' : 'row',
30+
}}
31+
>
2532
<Tab disabled={disabledIndex === 0}>Tab 1</Tab>
2633
<Tab disabled={disabledIndex === 1 || isMiddleDisabledSignal.value}>
2734
Tab 2
@@ -229,79 +236,165 @@ describe('Tabs', () => {
229236
});
230237
});
231238

232-
describe('Right key handling', () => {
233-
it(`GIVEN 3 tabs and the focus is on the first,
234-
WHEN triggering the right arrow key
235-
THEN the focus should be on the next tab`, () => {
236-
cy.mount(<ThreeTabsComponent />);
239+
describe('Orientation: Horizontal', () => {
240+
describe('RIGHT key handling', () => {
241+
it(`GIVEN 3 tabs and the focus is on the first,
242+
WHEN triggering the right arrow key
243+
THEN the focus should be on the next tab`, () => {
244+
cy.mount(<ThreeTabsComponent />);
237245

238-
cy.findByRole('tab', { name: /Tab 1/i }).type('{rightarrow}');
246+
cy.findByRole('tab', { name: /Tab 1/i }).type('{rightarrow}');
239247

240-
cy.findByRole('tab', { name: /Tab 2/i }).should('have.focus');
241-
});
248+
cy.findByRole('tab', { name: /Tab 2/i }).should('have.focus');
249+
});
242250

243-
it(`GIVEN 3 tabs and the focus is on the last,
244-
WHEN triggering the right arrow key
245-
THEN the focus should be on the first tab`, () => {
246-
cy.mount(<ThreeTabsComponent />);
251+
it(`GIVEN 3 tabs and the focus is on the last,
252+
WHEN triggering the right arrow key
253+
THEN the focus should be on the first tab`, () => {
254+
cy.mount(<ThreeTabsComponent />);
247255

248-
cy.findByRole('tab', { name: /Tab 3/i }).type('{rightarrow}');
256+
cy.findByRole('tab', { name: /Tab 3/i }).type('{rightarrow}');
249257

250-
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
251-
});
258+
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
259+
});
252260

253-
it(`GIVEN 3 tabs and the second is disabled and the focus is on the first,
254-
WHEN triggering the right arrow key
255-
THEN the focus should be on the third tab`, () => {
256-
cy.mount(<ThreeTabsComponent isMiddleDisabled={true} />);
261+
it(`GIVEN 3 tabs and the second is disabled and the focus is on the first,
262+
WHEN triggering the right arrow key
263+
THEN the focus should be on the third tab`, () => {
264+
cy.mount(<ThreeTabsComponent isMiddleDisabled={true} />);
257265

258-
cy.findByRole('tab', { name: /Tab 1/i }).type('{rightarrow}');
266+
cy.findByRole('tab', { name: /Tab 1/i }).type('{rightarrow}');
259267

260-
cy.findByRole('tab', { name: /Tab 3/i }).should('have.focus');
268+
cy.findByRole('tab', { name: /Tab 3/i }).should('have.focus');
269+
});
270+
271+
it(`GIVEN 3 tabs and the focus is on the first,
272+
WHEN disabling the middle dynamically and triggering the right arrow key
273+
THEN the focus should be on the third tab`, () => {
274+
cy.mount(<ThreeTabsComponent showDisableButton={true} />);
275+
276+
cy.findByRole('button', { name: 'Toggle middle tab disabled' }).click();
277+
278+
cy.findByRole('tab', { name: /Tab 1/i }).type('{rightarrow}');
279+
280+
cy.findByRole('tab', { name: /Tab 3/i }).should('have.focus');
281+
});
261282
});
262283

263-
it(`GIVEN 3 tabs and the focus is on the first,
264-
WHEN disabling the middle dynamically and triggering the right arrow key
265-
THEN the focus should be on the third tab`, () => {
266-
cy.mount(<ThreeTabsComponent showDisableButton={true} />);
284+
describe('LEFT key handling', () => {
285+
it(`GIVEN 3 tabs and the focus is on the second,
286+
WHEN triggering the left arrow key
287+
THEN the focus should be on the first tab`, () => {
288+
cy.mount(<ThreeTabsComponent />);
267289

268-
cy.findByRole('button', { name: 'Toggle middle tab disabled' }).click();
290+
cy.findByRole('tab', { name: /Tab 2/i }).type('{leftarrow}');
269291

270-
cy.findByRole('tab', { name: /Tab 1/i }).type('{rightarrow}');
292+
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
293+
});
271294

272-
cy.findByRole('tab', { name: /Tab 3/i }).should('have.focus');
295+
it(`GIVEN 3 tabs and the focus is on the first,
296+
WHEN triggering the left arrow key
297+
THEN the focus should be on the last tab`, () => {
298+
cy.mount(<ThreeTabsComponent />);
299+
300+
cy.findByRole('tab', { name: /Tab 1/i }).type('{leftarrow}');
301+
302+
cy.findByRole('tab', { name: /Tab 3/i }).should('have.focus');
303+
});
304+
305+
it(`GIVEN 3 tabs and the second is disabled and the focus is on the third,
306+
WHEN triggering the left arrow key
307+
THEN the focus should be on the first tab`, () => {
308+
cy.mount(<ThreeTabsComponent isMiddleDisabled={true} />);
309+
310+
cy.findByRole('tab', { name: /Tab 3/i }).type('{leftarrow}');
311+
312+
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
313+
});
273314
});
274315
});
275316

276-
describe('Left key handling', () => {
277-
it(`GIVEN 3 tabs and the focus is on the second,
278-
WHEN triggering the left arrow key
279-
THEN the focus should be on the first tab`, () => {
280-
cy.mount(<ThreeTabsComponent />);
317+
describe('Orientation: Vertical', () => {
318+
describe('DOWN key handling', () => {
319+
it(`GIVEN 3 vertical tabs and the focus is on the first,
320+
WHEN triggering the down arrow key
321+
THEN the focus should be on the next tab`, () => {
322+
cy.mount(<ThreeTabsComponent isVertical={true} />);
281323

282-
cy.findByRole('tab', { name: /Tab 2/i }).type('{leftarrow}');
324+
cy.findByRole('tab', { name: /Tab 1/i }).type('{downarrow}');
283325

284-
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
285-
});
326+
cy.findByRole('tab', { name: /Tab 2/i }).should('have.focus');
327+
});
286328

287-
it(`GIVEN 3 tabs and the focus is on the first,
288-
WHEN triggering the left arrow key
289-
THEN the focus should be on the last tab`, () => {
290-
cy.mount(<ThreeTabsComponent />);
329+
it(`GIVEN 3 vertical tabs and the focus is on the last,
330+
WHEN triggering the down arrow key
331+
THEN the focus should be on the first tab`, () => {
332+
cy.mount(<ThreeTabsComponent isVertical={true} />);
291333

292-
cy.findByRole('tab', { name: /Tab 1/i }).type('{leftarrow}');
334+
cy.findByRole('tab', { name: /Tab 3/i }).type('{downarrow}');
293335

294-
cy.findByRole('tab', { name: /Tab 3/i }).should('have.focus');
336+
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
337+
});
338+
339+
it(`GIVEN 3 vertical tabs and the second is disabled and the focus is on the first,
340+
WHEN triggering the down arrow key
341+
THEN the focus should be on the third tab`, () => {
342+
cy.mount(
343+
<ThreeTabsComponent isVertical={true} isMiddleDisabled={true} />
344+
);
345+
346+
cy.findByRole('tab', { name: /Tab 1/i }).type('{downarrow}');
347+
348+
cy.findByRole('tab', { name: /Tab 3/i }).should('have.focus');
349+
});
350+
351+
it(`GIVEN 3 vertical tabs and the focus is on the first,
352+
WHEN disabling the middle dynamically and triggering the down arrow key
353+
THEN the focus should be on the third tab`, () => {
354+
cy.mount(
355+
<ThreeTabsComponent isVertical={true} showDisableButton={true} />
356+
);
357+
358+
cy.findByRole('button', { name: 'Toggle middle tab disabled' }).click();
359+
360+
cy.findByRole('tab', { name: /Tab 1/i }).type('{downarrow}');
361+
362+
cy.findByRole('tab', { name: /Tab 3/i }).should('have.focus');
363+
});
295364
});
296365

297-
it(`GIVEN 3 tabs and the second is disabled and the focus is on the third,
298-
WHEN triggering the left arrow key
299-
THEN the focus should be on the first tab`, () => {
300-
cy.mount(<ThreeTabsComponent isMiddleDisabled={true} />);
366+
describe('UP key handling', () => {
367+
it(`GIVEN 3 vertical tabs and the focus is on the second,
368+
WHEN triggering the up arrow key
369+
THEN the focus should be on the first tab`, () => {
370+
cy.mount(<ThreeTabsComponent isVertical={true} />);
301371

302-
cy.findByRole('tab', { name: /Tab 3/i }).type('{leftarrow}');
372+
cy.findByRole('tab', { name: /Tab 2/i }).type('{uparrow}');
303373

304-
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
374+
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
375+
});
376+
377+
it(`GIVEN 3 vertical tabs and the focus is on the first,
378+
WHEN triggering the up arrow key
379+
THEN the focus should be on the last tab`, () => {
380+
cy.mount(<ThreeTabsComponent isVertical={true} />);
381+
382+
cy.findByRole('tab', { name: /Tab 1/i }).type('{uparrow}');
383+
384+
cy.findByRole('tab', { name: /Tab 3/i }).should('have.focus');
385+
});
386+
387+
it(`GIVEN 3 vertical tabs and the second is disabled and the focus is on the third,
388+
WHEN triggering the up arrow key
389+
THEN the focus should be on the first tab`, () => {
390+
cy.mount(
391+
<ThreeTabsComponent isVertical={true} isMiddleDisabled={true} />
392+
);
393+
394+
cy.findByRole('tab', { name: /Tab 3/i }).type('{uparrow}');
395+
396+
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
397+
});
305398
});
306399
});
307400

@@ -316,6 +409,16 @@ describe('Tabs', () => {
316409
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
317410
});
318411

412+
it(`GIVEN 3 vertical tabs and the focus is on the third,
413+
WHEN triggering the 'home' key
414+
THEN the focus should be on the first tab`, () => {
415+
cy.mount(<ThreeTabsComponent isVertical={true} />);
416+
417+
cy.findByRole('tab', { name: /Tab 3/i }).type('{home}');
418+
419+
cy.findByRole('tab', { name: /Tab 1/i }).should('have.focus');
420+
});
421+
319422
it(`GIVEN 3 tabs and the first is disabled and the focus is on the third,
320423
WHEN triggering the 'home' key
321424
THEN the focus should be on the second tab`, () => {

packages/kit-headless/src/components/tabs/tabs.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ import { KeyCode } from '../../utils/key-code.type';
1717
* TABS TODOs
1818
*
1919
* - onSelectedIndexChange
20-
* - Orientation
21-
* - keyboard interactions (arrowDown, ArrowUp, - and home, pagedown, up and end for vertical)
2220
2321
* aria Tabs Pattern https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
2422
* a11y lint plugin https://www.npmjs.com/package/eslint-plugin-jsx-a11y
@@ -37,6 +35,7 @@ export interface TabsProps {
3735
behavior?: Behavior;
3836
class?: string;
3937
selectedIndex?: number;
38+
vertical?: boolean;
4039
}
4140

4241
export interface TabPair {
@@ -95,7 +94,10 @@ export const Tabs = component$((props: TabsProps) => {
9594
(tabPair) => tabPair.tabId === tabId
9695
);
9796

98-
if (key === KeyCode.ArrowRight) {
97+
if (
98+
key === KeyCode.ArrowRight ||
99+
(props.vertical && key === KeyCode.ArrowDown)
100+
) {
99101
let nextTabId = enabledTabs[0].tabId;
100102

101103
if (currentFocusedTabIndex < tabPairs.length - 1) {
@@ -106,7 +108,10 @@ export const Tabs = component$((props: TabsProps) => {
106108
?.focus();
107109
}
108110

109-
if (key === KeyCode.ArrowLeft) {
111+
if (
112+
key === KeyCode.ArrowLeft ||
113+
(props.vertical && key === KeyCode.ArrowUp)
114+
) {
110115
let previousTabId = enabledTabs[enabledTabs.length - 1].tabId;
111116

112117
if (currentFocusedTabIndex !== 0) {

0 commit comments

Comments
 (0)