-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9fc7f74
commit 4097e73
Showing
3 changed files
with
142 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# Tabs | ||
|
||
**A custom element for building accessible tabbed interfaces.** | ||
|
||
## Example | ||
|
||
```js | ||
import { TabsElement } from 'inclusive-elements'; | ||
|
||
window.customElements.define('ui-tabs', TabsElement); | ||
``` | ||
|
||
```html | ||
<ui-tabs> | ||
<div role="tablist" aria-label="Tabs"> | ||
<button type="button" role="tab">Tab 1</button> | ||
<button type="button" role="tab">Tab 2</button> | ||
<button type="button" role="tab">Tab 3</button> | ||
</div> | ||
<div role="tabpanel">Tab Panel 1</div> | ||
<div role="tabpanel" hidden>Tab Panel 2</div> | ||
<div role="tabpanel" hidden>Tab Panel 3</div> | ||
</ui-tabs> | ||
``` | ||
|
||
## Behavior | ||
|
||
- Descendants with `role="tab"` and `role="tabpanel"` will have appropriate `id`, `aria-controls`, and `aria-labelledby` attributes generated if they are not already set. | ||
|
||
- The active `tab` will have the `aria-selected="true"` attribute set. Inactive tabs will have their `tabindex` set to `-1` so that focus remains on the active tab. | ||
|
||
- When focus is on the active `tab`, pressing the `Left Arrow`, `Right Arrow`, `Home`, and `End` keys can be used for navigation. If the `tablist` has `aria-orientation="vertical"`, `Down Arrow` and `Up Arrow` are used instead. | ||
|
||
- The `tab` with focus is automatically activated, and its corresponding `tabpanel` will become visible. | ||
|
||
## Further Reading | ||
|
||
- [ARIA Authoring Practices Guide: Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
let idCounter = 0; | ||
|
||
export default class TabsElement extends HTMLElement { | ||
private idPrefix = 'tabs' + ++idCounter; | ||
|
||
public connectedCallback(): void { | ||
this.tablist.addEventListener('keydown', this.onKeyDown); | ||
this.tablist.addEventListener('click', this.onClick); | ||
|
||
this.selectTab(0, false); | ||
|
||
this.tabs.forEach((tab, i) => { | ||
const panel = this.tabpanels[i]; | ||
const tabId = tab.getAttribute('id') || this.idPrefix + '_tab_' + i; | ||
const panelId = | ||
panel.getAttribute('id') || this.idPrefix + '_panel_' + i; | ||
|
||
tab.setAttribute('id', tabId); | ||
tab.setAttribute('aria-controls', panelId); | ||
|
||
panel.setAttribute('id', panelId); | ||
panel.setAttribute('aria-labelledby', tabId); | ||
panel.setAttribute('tabindex', '0'); | ||
}); | ||
} | ||
|
||
public disconnectedCallback(): void { | ||
this.tablist.removeEventListener('keydown', this.onKeyDown); | ||
this.tablist.removeEventListener('click', this.onClick); | ||
} | ||
|
||
private selectTab(index: number, focus = true) { | ||
if (index < 0) index += this.tabs.length; | ||
else if (index >= this.tabs.length) index -= this.tabs.length; | ||
|
||
this.tabs.forEach((tab, i) => { | ||
if (i === index) { | ||
tab.setAttribute('aria-selected', 'true'); | ||
tab.removeAttribute('tabindex'); | ||
this.tabpanels[i].hidden = false; | ||
if (focus) tab.focus(); | ||
} else { | ||
tab.setAttribute('aria-selected', 'false'); | ||
tab.setAttribute('tabindex', '-1'); | ||
this.tabpanels[i].hidden = true; | ||
} | ||
}); | ||
} | ||
|
||
private onKeyDown = (e: KeyboardEvent) => { | ||
const target = e.target as HTMLElement; | ||
const index = this.tabs.indexOf(target); | ||
const vertical = | ||
this.tablist.getAttribute('aria-orientation') === 'vertical'; | ||
let captured = false; | ||
|
||
switch (e.key) { | ||
case vertical ? 'ArrowUp' : 'ArrowLeft': | ||
this.selectTab(index - 1); | ||
captured = true; | ||
break; | ||
|
||
case vertical ? 'ArrowDown' : 'ArrowRight': | ||
this.selectTab(index + 1); | ||
captured = true; | ||
break; | ||
|
||
case 'Home': | ||
this.selectTab(0); | ||
captured = true; | ||
break; | ||
|
||
case 'End': | ||
this.selectTab(this.tabs.length - 1); | ||
captured = true; | ||
} | ||
|
||
if (captured) { | ||
e.stopPropagation(); | ||
e.preventDefault(); | ||
} | ||
}; | ||
|
||
private onClick = (e: MouseEvent) => { | ||
const target = e.target as HTMLElement; | ||
const tab = target.closest<HTMLElement>('[role=tab]'); | ||
if (tab) { | ||
this.selectTab(this.tabs.indexOf(tab)); | ||
} | ||
}; | ||
|
||
private get tablist(): HTMLElement { | ||
return this.querySelector('[role=tablist]')!; | ||
} | ||
|
||
private get tabs(): HTMLElement[] { | ||
return Array.from(this.querySelectorAll('[role=tab]')); | ||
} | ||
|
||
private get tabpanels(): HTMLElement[] { | ||
return Array.from(this.querySelectorAll('[role=tabpanel]')); | ||
} | ||
} |