feat: accessible tab kbd navigation

This commit is contained in:
tglide
2023-05-10 19:00:19 +01:00
parent 2cc2c57b5e
commit ce523b316b
2 changed files with 81 additions and 2 deletions
+65 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { getElementDir } from '$lib/helpers/style';
export let selected = false;
export let href: string = null;
@@ -9,11 +10,70 @@
if (!event) return;
trackEvent(`click_select_tab_${event}`);
}
const keysMap = {
ltr: {
next: 'ArrowRight',
prev: 'ArrowLeft'
},
rtl: {
next: 'ArrowLeft',
prev: 'ArrowRight'
}
};
function handleKeyDown(e: KeyboardEvent) {
const tabBtn = e.target as HTMLElement;
const tabItem = tabBtn.closest('.tabs-item') as HTMLElement;
const tabsList = tabBtn.closest('.tabs-item').closest('.tabs') as HTMLElement;
const tabItems = tabsList.querySelectorAll('.tabs-item');
const currentIdx = Array.from(tabItems).indexOf(tabItem);
const dir = getElementDir(tabsList);
switch (e.key) {
case 'Home': {
e.preventDefault();
const firstTabBtn = tabItems[0].querySelector('.tabs-button') as HTMLElement;
firstTabBtn.focus();
break;
}
case 'End': {
e.preventDefault();
const lastTabBtn = tabItems[tabItems.length - 1].querySelector(
'.tabs-button'
) as HTMLElement;
lastTabBtn.focus();
break;
}
case keysMap[dir].next: {
e.preventDefault();
const nextIdx = currentIdx === tabItems.length - 1 ? 0 : currentIdx + 1;
const nextTabBtn = tabItems[nextIdx].querySelector('.tabs-button') as HTMLElement;
nextTabBtn.focus();
break;
}
case keysMap[dir].prev: {
e.preventDefault();
const prevIdx = currentIdx === 0 ? tabItems.length - 1 : currentIdx - 1;
const prevTabBtn = tabItems[prevIdx].querySelector('.tabs-button') as HTMLElement;
prevTabBtn.focus();
break;
}
}
}
</script>
<li class="tabs-item">
{#if href}
<a class="tabs-button" {href} class:is-selected={selected} on:click={track}>
<a
class="tabs-button"
{href}
class:is-selected={selected}
on:click={track}
tabindex={selected ? 0 : -1}
on:keydown={handleKeyDown}
role="tab">
<span class="text"><slot /></span>
</a>
{:else}
@@ -22,7 +82,10 @@
class="tabs-button"
class:is-selected={selected}
on:click|preventDefault
on:click={track}>
on:click={track}
tabindex={selected ? 0 : -1}
on:keydown={handleKeyDown}
role="tab">
<span class="text"><slot /></span>
</button>
{/if}
+16
View File
@@ -0,0 +1,16 @@
type Direction = 'rtl' | 'ltr';
function isDirection(dir: string): dir is Direction {
return dir === 'rtl' || dir === 'ltr';
}
function parseDirection(dir: string): Direction {
return isDirection(dir) ? dir : 'ltr';
}
export function getElementDir(el: HTMLElement): Direction {
if (window.getComputedStyle) {
return parseDirection(window.getComputedStyle(el, null).getPropertyValue('direction'));
}
return parseDirection(el.style.direction);
}