UNPKG

melt

Version:

The next generation of Melt UI. Built for Svelte 5.

113 lines (112 loc) 4.41 kB
import { dataAttr } from "../utils/attribute"; import { extract } from "../utils/extract"; import { Synced } from "../Synced.svelte"; import { createDataIds, createId } from "../utils/identifiers"; import { isHtmlElement } from "../utils/is"; const TRIGGER_KEYS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"]; const identifiers = createDataIds("tabs", ["trigger", "content", "trigger-list"]); export class Tabs { #value; #id = createId(); /* Props */ #props; selectWhenFocused = $derived(extract(this.#props.selectWhenFocused, true)); loop = $derived(extract(this.#props.loop, true)); orientation = $derived(extract(this.#props.orientation, "horizontal")); constructor(props) { this.#props = props; this.#value = new Synced({ value: props.value, onChange: props.onValueChange, }); } #getTriggerId(value) { return `${this.#id}-trigger-${value.replace(/\s/g, "_")}`; } #getContentId(value) { return `${this.#id}-content-${value.replace(/\s/g, "_")}`; } /** The current selected tab. */ get value() { return this.#value.current; } set value(value) { this.#value.current = value; } /** The attributes for the list that contains the tab triggers. */ get triggerList() { return { [identifiers["trigger-list"]]: "", role: "tablist", "aria-orientation": this.orientation, "data-orientation": this.orientation, }; } /** Gets the attributes and listeners for a tab trigger. Requires an identifying tab value. */ getTrigger(value) { return { [identifiers.trigger]: value, "data-active": dataAttr(this.value === value), tabindex: this.value === value ? 0 : -1, role: "tab", "aria-selected": this.value === value, "aria-controls": this.#getContentId(value), "data-orientation": this.orientation, onclick: () => (this.value = value), onkeydown: (e) => { const el = e.target; if (!TRIGGER_KEYS.includes(e.key) || !isHtmlElement(el)) { return; } e.preventDefault(); const triggerList = el.closest(`[${identifiers["trigger-list"]}]`); if (!triggerList) return; const triggers = [...triggerList.querySelectorAll(`[${identifiers.trigger}]`)]; const currIndex = triggers.indexOf(el); let next = el; const prevKey = this.orientation === "horizontal" ? "ArrowLeft" : "ArrowUp"; const nextKey = this.orientation === "horizontal" ? "ArrowRight" : "ArrowDown"; switch (e.key) { case prevKey: { next = this.loop ? triggers.at(currIndex - 1) : triggers.at(Math.max(currIndex - 1, 0)); break; } case nextKey: { next = this.loop ? triggers.at((currIndex + 1) % triggers.length) : triggers.at(currIndex + 1); break; } case "Home": { next = triggers[0]; break; } case "End": { next = triggers.at(-1); break; } } if (!isHtmlElement(next)) return; next.focus(); if (this.selectWhenFocused) { this.value = next.getAttribute(identifiers.trigger); } }, id: this.#getTriggerId(value), }; } /** Gets the attributes and listeners for the tabs contents. Requires an identifying tab value. */ getContent(value) { return { [identifiers.content]: "", hidden: this.value !== value, "data-active": dataAttr(this.value === value), role: "tabpanel", id: this.#getContentId(value), "aria-labelledby": this.#getTriggerId(value), "data-orientation": this.orientation, }; } }