Type fixes, support a couple more classes as functions for the examples, and switch TabPanels to registering ids instead of refs
128 lines
3.2 KiB
Svelte
128 lines
3.2 KiB
Svelte
<script lang="ts" context="module">
|
|
import {
|
|
createEventDispatcher,
|
|
getContext,
|
|
onMount,
|
|
setContext,
|
|
} from "svelte";
|
|
|
|
import { writable, Writable } from "svelte/store";
|
|
|
|
export type StateDefinition = {
|
|
// State
|
|
selectedIndex: number | null;
|
|
orientation: "vertical" | "horizontal";
|
|
activation: "auto" | "manual";
|
|
|
|
tabs: (HTMLElement | null)[];
|
|
panels: string[];
|
|
|
|
// State mutators
|
|
setSelectedIndex(index: number): void;
|
|
registerTab(tab: HTMLElement | null): void;
|
|
unregisterTab(tab: HTMLElement | null): void;
|
|
registerPanel(panel: string): void;
|
|
unregisterPanel(panel: string): void;
|
|
};
|
|
|
|
const TABS_CONTEXT_NAME = "TabsContext";
|
|
|
|
export function useTabsContext(component: string): Writable<StateDefinition> {
|
|
let context: Writable<StateDefinition> | undefined =
|
|
getContext(TABS_CONTEXT_NAME);
|
|
|
|
if (context === undefined) {
|
|
throw new Error(
|
|
`<${component} /> is missing a parent <TabGroup /> component.`
|
|
);
|
|
}
|
|
|
|
return context;
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
export let defaultIndex = 0;
|
|
export let vertical = false;
|
|
export let manual = false;
|
|
|
|
let selectedIndex: StateDefinition["selectedIndex"] = null;
|
|
let tabs: StateDefinition["tabs"] = [];
|
|
let panels: StateDefinition["panels"] = [];
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
let api: Writable<StateDefinition> = writable({
|
|
selectedIndex,
|
|
orientation: vertical ? "vertical" : "horizontal",
|
|
activation: manual ? "manual" : "auto",
|
|
tabs,
|
|
panels,
|
|
setSelectedIndex(index: number) {
|
|
if (selectedIndex === index) return;
|
|
selectedIndex = index;
|
|
dispatch("updateValue", index);
|
|
},
|
|
registerTab(tab: typeof tabs[number]) {
|
|
if (!tabs.includes(tab)) tabs = [...tabs, tab];
|
|
},
|
|
unregisterTab(tab: typeof tabs[number]) {
|
|
tabs = tabs.filter((t) => t !== tab);
|
|
},
|
|
registerPanel(panel: typeof panels[number]) {
|
|
if (!panels.includes(panel)) panels = [...panels, panel];
|
|
},
|
|
unregisterPanel(panel: typeof panels[number]) {
|
|
panels = panels.filter((p) => p !== panel);
|
|
},
|
|
});
|
|
setContext(TABS_CONTEXT_NAME, api);
|
|
|
|
$: api.update((obj) => {
|
|
return {
|
|
...obj,
|
|
selectedIndex,
|
|
orientation: vertical ? "vertical" : "horizontal",
|
|
activation: manual ? "manual" : "auto",
|
|
tabs,
|
|
panels,
|
|
};
|
|
});
|
|
|
|
onMount(() => {
|
|
if ($api.tabs.length <= 0) return;
|
|
if (selectedIndex !== null) return;
|
|
|
|
let tabs = $api.tabs.filter(Boolean) as HTMLElement[];
|
|
let focusableTabs = tabs.filter((tab) => !tab.hasAttribute("disabled"));
|
|
if (focusableTabs.length <= 0) return;
|
|
|
|
// Underflow
|
|
if (defaultIndex < 0) {
|
|
selectedIndex = tabs.indexOf(focusableTabs[0]);
|
|
}
|
|
|
|
// Overflow
|
|
else if (defaultIndex > $api.tabs.length) {
|
|
selectedIndex = tabs.indexOf(focusableTabs[focusableTabs.length - 1]);
|
|
}
|
|
|
|
// Middle
|
|
else {
|
|
let before = tabs.slice(0, defaultIndex);
|
|
let after = tabs.slice(defaultIndex);
|
|
|
|
let next = [...after, ...before].find((tab) =>
|
|
focusableTabs.includes(tab)
|
|
);
|
|
if (!next) return;
|
|
|
|
selectedIndex = tabs.indexOf(next);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div {...$$restProps}>
|
|
<slot {selectedIndex} />
|
|
</div>
|