Skip to content

Commit 7eb3298

Browse files
authored
Merge branch 'main' into refactor-gettabs-into-private-getters
2 parents 09947d8 + 9993b1d commit 7eb3298

3 files changed

Lines changed: 111 additions & 65 deletions

File tree

examples/index.html

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,58 @@
66
<link rel="stylesheet" href="tabs.css" />
77
</head>
88
<body>
9+
<h1>Tab Container Examples</h1>
10+
11+
<h2>Horizontal</h2>
12+
13+
<tab-container>
14+
<div role="tablist" aria-label="Horizontal Tabs Example">
15+
<button type="button" id="tab-one" role="tab">Tab one</button>
16+
<button type="button" id="tab-two" role="tab">Tab two</button>
17+
<button type="button" id="tab-three" role="tab">Tab three</button>
18+
</div>
19+
<div role="tabpanel" aria-labelledby="tab-one">
20+
Panel 1
21+
</div>
22+
<div role="tabpanel" aria-labelledby="tab-two" hidden>
23+
Panel 2
24+
</div>
25+
<div role="tabpanel" aria-labelledby="tab-three" hidden>
26+
Panel 3
27+
</div>
28+
</tab-container>
29+
30+
<h2>Vertical</h2>
31+
32+
<tab-container>
33+
<div role="tablist" aria-label="Vertical Tabs Example" aria-orientation="vertical">
34+
<button type="button" id="tab-one" role="tab">Tab one</button>
35+
<button type="button" id="tab-two" role="tab">Tab two</button>
36+
<button type="button" id="tab-three" role="tab">Tab three</button>
37+
</div>
38+
<div role="tabpanel" aria-labelledby="tab-one">
39+
Panel 1
40+
</div>
41+
<div role="tabpanel" aria-labelledby="tab-two" hidden>
42+
Panel 2
43+
</div>
44+
<div role="tabpanel" aria-labelledby="tab-three" hidden>
45+
Panel 3
46+
</div>
47+
</tab-container>
48+
49+
<h2>Panel with extra buttons</h2>
50+
951
<tab-container>
10-
<div role="tablist">
11-
<button type="button" id="tab-one" role="tab" tabindex="0" aria-selected="true">Tab one</button>
12-
<button type="button" id="tab-two" role="tab" tabindex="-1">Tab two</button>
13-
<button type="button" id="tab-three" role="tab" tabindex="-1">Tab three</button>
52+
<div style="display: flex">
53+
<button>Left button, not a tab!</button>
54+
<button>2nd Left button, not a tab!</button>
55+
<div role="tablist" aria-label="Tabs Example with extra buttons">
56+
<button type="button" id="tab-one" role="tab">Tab one</button>
57+
<button type="button" id="tab-two" role="tab">Tab two</button>
58+
<button type="button" id="tab-three" role="tab">Tab three</button>
59+
</div>
60+
<button>Right button, not a tab!</button>
1461
</div>
1562
<div role="tabpanel" aria-labelledby="tab-one">
1663
Panel 1
@@ -21,6 +68,7 @@
2168
<div role="tabpanel" aria-labelledby="tab-three" hidden>
2269
Panel 3
2370
</div>
71+
<p>This comes after the panels</p>
2472
</tab-container>
2573

2674
<!-- <script src="../dist/index.js" type="module"></script> -->

examples/tabs.css

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1-
/* Borrowed from https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html */
1+
/* Borrowed (and modified) from https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html */
22

3-
[role="tablist"] {
4-
margin: 0 0 -.1em;
5-
overflow: visible;
3+
tab-container {
4+
width: 100%;
5+
margin: 2rem 0;
6+
}
7+
8+
tab-container>[role="tablist"][aria-orientation="vertical"] {
9+
display: flex;
10+
flex-direction: column;
11+
}
12+
tab-container:has([aria-orientation="vertical"]) {
13+
display: flex;
14+
flex-direction: row;
15+
}
16+
tab-container:has([aria-orientation="vertical"]) [role="tabpanel"] {
17+
flex: 1;
618
}
719

820
[role="tab"] {

src/tab-container-element.ts

Lines changed: 43 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
const HTMLElement = globalThis.HTMLElement || (null as unknown as (typeof window)['HTMLElement'])
22

3-
type IncrementKeyCode = 'ArrowRight' | 'ArrowDown'
4-
type DecrementKeyCode = 'ArrowUp' | 'ArrowLeft'
5-
63
export class TabContainerChangeEvent extends Event {
74
constructor(type: string, {tab, panel, ...init}: EventInit & {tab?: Element; panel?: Element}) {
85
super(type, init)
@@ -27,17 +24,6 @@ export class TabContainerChangeEvent extends Event {
2724
}
2825
}
2926

30-
function getNavigationKeyCodes(vertical: boolean): [IncrementKeyCode[], DecrementKeyCode[]] {
31-
if (vertical) {
32-
return [
33-
['ArrowDown', 'ArrowRight'],
34-
['ArrowUp', 'ArrowLeft'],
35-
]
36-
} else {
37-
return [['ArrowRight'], ['ArrowLeft']]
38-
}
39-
}
40-
4127
export class TabContainerElement extends HTMLElement {
4228
static define(tag = 'tab-container', registry = customElements) {
4329
registry.define(tag, this)
@@ -91,49 +77,8 @@ export class TabContainerElement extends HTMLElement {
9177
}
9278

9379
connectedCallback(): void {
94-
this.addEventListener('keydown', (event: KeyboardEvent) => {
95-
const target = event.target
96-
if (!(target instanceof HTMLElement)) return
97-
if (target.closest(this.tagName) !== this) return
98-
if (target.getAttribute('role') !== 'tab' && !target.closest('[role="tablist"]')) return
99-
const tabs = this.#tabs
100-
const currentIndex = tabs.indexOf(tabs.find(tab => tab.matches('[aria-selected="true"]'))!)
101-
const [incrementKeys, decrementKeys] = getNavigationKeyCodes(
102-
target.closest('[role="tablist"]')?.getAttribute('aria-orientation') === 'vertical',
103-
)
104-
105-
if (incrementKeys.some(code => event.code === code)) {
106-
let index = currentIndex + 1
107-
if (index >= tabs.length) index = 0
108-
this.selectTab(index)
109-
} else if (decrementKeys.some(code => event.code === code)) {
110-
let index = currentIndex - 1
111-
if (index < 0) index = tabs.length - 1
112-
this.selectTab(index)
113-
} else if (event.code === 'Home') {
114-
this.selectTab(0)
115-
event.preventDefault()
116-
} else if (event.code === 'End') {
117-
this.selectTab(tabs.length - 1)
118-
event.preventDefault()
119-
}
120-
})
121-
122-
this.addEventListener('click', (event: MouseEvent) => {
123-
const tabs = this.#tabs
124-
125-
if (!(event.target instanceof Element)) return
126-
if (event.target.closest(this.tagName) !== this) return
127-
128-
const tab = event.target.closest('[role="tab"]')
129-
if (!(tab instanceof HTMLElement) || !tab.closest('[role="tablist"]')) {
130-
return
131-
}
132-
133-
const index = tabs.indexOf(tab)
134-
this.selectTab(index)
135-
})
136-
80+
this.addEventListener('keydown', this)
81+
this.addEventListener('click', this)
13782
for (const tab of this.#tabs) {
13883
if (!tab.hasAttribute('aria-selected')) {
13984
tab.setAttribute('aria-selected', 'false')
@@ -148,6 +93,47 @@ export class TabContainerElement extends HTMLElement {
14893
}
14994
}
15095

96+
handleEvent(event: Event) {
97+
if (event.type === 'click') return this.#handleClick(event as MouseEvent)
98+
if (event.type === 'keydown') return this.#handleKeydown(event as KeyboardEvent)
99+
}
100+
101+
#handleKeydown(event: KeyboardEvent) {
102+
const tab = (event.target as HTMLElement)?.closest?.('[role="tab"]')
103+
if (!tab) return
104+
const tabs = getTabs(this)
105+
if (!tabs.includes(tab as HTMLElement)) return
106+
107+
const currentIndex = tabs.indexOf(tabs.find(e => e.matches('[aria-selected="true"]'))!)
108+
const vertical = tab.closest('[role="tablist"]')?.getAttribute('aria-orientation') === 'vertical'
109+
const prevTab = event.code === 'ArrowLeft' || (vertical && event.code === 'ArrowUp')
110+
const nextTab = event.code === 'ArrowRight' || (vertical && event.code === 'ArrowDown')
111+
112+
if (nextTab) {
113+
let index = currentIndex + 1
114+
if (index >= tabs.length) index = 0
115+
this.selectTab(index)
116+
} else if (prevTab) {
117+
let index = currentIndex - 1
118+
if (index < 0) index = tabs.length - 1
119+
this.selectTab(index)
120+
} else if (event.code === 'Home') {
121+
this.selectTab(0)
122+
event.preventDefault()
123+
} else if (event.code === 'End') {
124+
this.selectTab(tabs.length - 1)
125+
event.preventDefault()
126+
}
127+
}
128+
129+
#handleClick(event: MouseEvent) {
130+
const tab = (event.target as HTMLElement)?.closest?.('[role=tab]')
131+
if (!tab) return
132+
const tabs = getTabs(this)
133+
const index = tabs.indexOf(tab as HTMLElement)
134+
if (index >= 0) this.selectTab(index)
135+
}
136+
151137
selectTab(index: number): void {
152138
const tabs = this.#tabs
153139
const panels = Array.from(this.querySelectorAll<HTMLElement>('[role="tabpanel"]')).filter(

0 commit comments

Comments
 (0)