Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 84 additions & 5 deletions src/lib/components/tab.svelte
Original file line number Diff line number Diff line change
@@ -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}
Expand All @@ -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}
Expand Down
16 changes: 16 additions & 0 deletions src/lib/helpers/style.ts
Original file line number Diff line number Diff line change
@@ -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);
}
14 changes: 14 additions & 0 deletions src/lib/helpers/waitUntil.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 3 deletions tests/unit/components/tab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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);

Expand Down