mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-07 22:03:14 -04:00
105 lines
3.3 KiB
Svelte
105 lines
3.3 KiB
Svelte
<script lang="ts">
|
|
import { tick } from "svelte";
|
|
import type { Component } from "svelte";
|
|
|
|
type Item = { id: string; label: string; href?: string; count?: number; icon?: Component<any> };
|
|
|
|
let {
|
|
items,
|
|
active,
|
|
label,
|
|
class: className = "",
|
|
onchange,
|
|
}: {
|
|
items: readonly Item[];
|
|
active: string;
|
|
label: string;
|
|
class?: string;
|
|
onchange?: (id: string) => void;
|
|
} = $props();
|
|
let tablist = $state<HTMLElement>();
|
|
|
|
$effect(() => {
|
|
const selectedId = active;
|
|
if (typeof document === "undefined") return;
|
|
|
|
let cancelled = false;
|
|
let frame = 0;
|
|
void tick().then(() => {
|
|
if (cancelled || active !== selectedId) return;
|
|
frame = requestAnimationFrame(() => {
|
|
const selected = tablist?.querySelector<HTMLElement>('[aria-selected="true"]');
|
|
if (!tablist || !selected) return;
|
|
const bounds = tablist.getBoundingClientRect();
|
|
const selectedBounds = selected.getBoundingClientRect();
|
|
const next = selected.nextElementSibling as HTMLElement | null;
|
|
const nextBounds = next?.getBoundingClientRect();
|
|
const end = nextBounds && nextBounds.right - selectedBounds.left <= bounds.width
|
|
? nextBounds.right
|
|
: selectedBounds.right;
|
|
if (selectedBounds.left < bounds.left)
|
|
tablist.scrollTo({ left: tablist.scrollLeft - (bounds.left - selectedBounds.left) });
|
|
else if (end > bounds.right)
|
|
tablist.scrollTo({ left: tablist.scrollLeft + end - bounds.right });
|
|
});
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (frame) cancelAnimationFrame(frame);
|
|
};
|
|
});
|
|
|
|
function navigate(event: KeyboardEvent) {
|
|
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
|
|
const tabs = [...(event.currentTarget as HTMLElement).querySelectorAll<HTMLElement>('[role="tab"]')];
|
|
const current = tabs.indexOf(document.activeElement as HTMLElement);
|
|
if (current < 0) return;
|
|
event.preventDefault();
|
|
const next = event.key === "Home"
|
|
? 0
|
|
: event.key === "End"
|
|
? tabs.length - 1
|
|
: (current + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
|
|
tabs[next].click();
|
|
tabs[next].focus();
|
|
}
|
|
</script>
|
|
|
|
{#snippet tabContent(item: Item)}
|
|
{@const Icon = item.icon}
|
|
{#if Icon}<Icon class="segmented-tab-icon" size={16} aria-hidden="true" />{/if}
|
|
<span class="segmented-tab-label">{item.label}</span>
|
|
{#if item.count !== undefined}<span class="segmented-tab-count">{item.count}</span>{/if}
|
|
{/snippet}
|
|
|
|
<nav class="segmented-nav" aria-label={label}>
|
|
<div
|
|
bind:this={tablist}
|
|
class={`segmented-tabs ${className}`}
|
|
role="tablist"
|
|
tabindex="-1"
|
|
style={`--tab-count:${items.length}`}
|
|
onkeydown={navigate}
|
|
>
|
|
{#each items as item}
|
|
{#if item.href}
|
|
<a
|
|
href={item.href}
|
|
role="tab"
|
|
aria-selected={active === item.id}
|
|
tabindex={active === item.id ? 0 : -1}
|
|
>{@render tabContent(item)}</a>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={active === item.id}
|
|
tabindex={active === item.id ? 0 : -1}
|
|
onclick={() => onchange?.(item.id)}
|
|
>{@render tabContent(item)}</button>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
</nav>
|