mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #437 from appwrite/feat/accessible-tabs
feat: accessible tab kbd navigation
This commit is contained in:
@@ -1,19 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { last } from '$lib/helpers/array';
|
||||
import { getElementDir } from '$lib/helpers/style';
|
||||
import { waitUntil } from '$lib/helpers/waitUntil';
|
||||
|
||||
export let selected = false;
|
||||
export let href: string = null;
|
||||
export let event: string = null;
|
||||
|
||||
function track() {
|
||||
if (!event) return;
|
||||
trackEvent(`click_select_tab_${event}`);
|
||||
async function handleClick(e: Event) {
|
||||
if (event) {
|
||||
trackEvent(`click_select_tab_${event}`);
|
||||
}
|
||||
|
||||
if (href) {
|
||||
let el = e.target as HTMLElement;
|
||||
if (el.tagName !== 'A') {
|
||||
el = el.closest('a');
|
||||
}
|
||||
|
||||
await goto(href);
|
||||
await waitUntil(() => {
|
||||
console.log('tickUntil', el);
|
||||
return el.classList.contains('is-selected');
|
||||
}, 1000);
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
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 = tabItem.closest('.tabs') as HTMLElement;
|
||||
const tabItems = Array.from(tabsList.querySelectorAll('.tabs-item'));
|
||||
const currentIdx = 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 = last(tabItems).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={handleClick}
|
||||
tabindex={selected ? 0 : -1}
|
||||
on:keydown={handleKeyDown}
|
||||
role="tab">
|
||||
<span class="text"><slot /></span>
|
||||
</a>
|
||||
{:else}
|
||||
@@ -22,7 +98,10 @@
|
||||
class="tabs-button"
|
||||
class:is-selected={selected}
|
||||
on:click|preventDefault
|
||||
on:click={track}>
|
||||
on:click={handleClick}
|
||||
tabindex={selected ? 0 : -1}
|
||||
on:keydown={handleKeyDown}
|
||||
role="tab">
|
||||
<span class="text"><slot /></span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export async function waitUntil(condition: () => boolean, timeout = 1000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
if (condition()) {
|
||||
clearInterval(interval);
|
||||
resolve(undefined);
|
||||
} else if (Date.now() - start > timeout) {
|
||||
clearInterval(interval);
|
||||
reject(new Error('Timeout'));
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
@@ -6,12 +6,12 @@ import { Tab } from '../../../src/lib/components';
|
||||
test('shows tab', () => {
|
||||
const { getByRole } = render(Tab);
|
||||
|
||||
expect(getByRole('button')).toBeInTheDocument();
|
||||
expect(getByRole('tab')).toBeInTheDocument();
|
||||
});
|
||||
test('shows tab - is selected', () => {
|
||||
const { getByRole } = render(Tab, { selected: true });
|
||||
|
||||
expect(getByRole('button')).toHaveClass('is-selected');
|
||||
expect(getByRole('tab')).toHaveClass('is-selected');
|
||||
});
|
||||
|
||||
test('shows tab - is link', () => {
|
||||
@@ -23,7 +23,7 @@ test('shows tab - is link', () => {
|
||||
|
||||
test('shows tab - on:click', async () => {
|
||||
const { getByRole, component } = render(Tab);
|
||||
const tab = getByRole('button');
|
||||
const tab = getByRole('tab');
|
||||
const callback = vi.fn();
|
||||
component.$on('click', callback);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user