When this runs, $api.tabs has not been updated yet, so the calculation was not correct. This caused various bugs like defaultIndex not working and aria-selected not being set, etc.
154 lines
4.0 KiB
Svelte
154 lines
4.0 KiB
Svelte
<script lang="ts" context="module">
|
|
interface PanelData {
|
|
id: string;
|
|
ref: Readable<HTMLElement | null>;
|
|
}
|
|
export type StateDefinition = {
|
|
// State
|
|
selectedIndex: number | null;
|
|
orientation: "vertical" | "horizontal";
|
|
activation: "auto" | "manual";
|
|
|
|
tabs: (HTMLElement | null)[];
|
|
panels: PanelData[];
|
|
|
|
// State mutators
|
|
setSelectedIndex(index: number): void;
|
|
registerTab(tab: HTMLElement | null): void;
|
|
unregisterTab(tab: HTMLElement | null): void;
|
|
registerPanel(panel: PanelData): void;
|
|
unregisterPanel(panel: PanelData): void;
|
|
};
|
|
|
|
const TABS_CONTEXT_NAME = "headlessui-tabs-context";
|
|
|
|
export function useTabsContext(component: string): Readable<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">
|
|
import {
|
|
createEventDispatcher,
|
|
getContext,
|
|
onMount,
|
|
setContext,
|
|
} from "svelte";
|
|
|
|
import { Readable, writable, Writable } from "svelte/store";
|
|
import { forwardEventsBuilder } from "$lib/internal/forwardEventsBuilder";
|
|
import { get_current_component } from "svelte/internal";
|
|
import type { SupportedAs } from "$lib/internal/elements";
|
|
import type { HTMLActionArray } from "$lib/hooks/use-actions";
|
|
import Render from "$lib/utils/Render.svelte";
|
|
const forwardEvents = forwardEventsBuilder(get_current_component(), [
|
|
"change",
|
|
]);
|
|
|
|
export let as: SupportedAs = "div";
|
|
export let use: HTMLActionArray = [];
|
|
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("change", 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 (tabs.length <= 0) return;
|
|
if (selectedIndex !== null) return;
|
|
|
|
let mountedTabs = tabs.filter(Boolean) as HTMLElement[];
|
|
let focusableTabs = mountedTabs.filter(
|
|
(tab) => !tab.hasAttribute("disabled")
|
|
);
|
|
if (focusableTabs.length <= 0) return;
|
|
|
|
// Underflow
|
|
if (defaultIndex < 0) {
|
|
selectedIndex = mountedTabs.indexOf(focusableTabs[0]);
|
|
}
|
|
|
|
// Overflow
|
|
else if (defaultIndex > mountedTabs.length) {
|
|
selectedIndex = mountedTabs.indexOf(
|
|
focusableTabs[focusableTabs.length - 1]
|
|
);
|
|
}
|
|
|
|
// Middle
|
|
else {
|
|
let before = mountedTabs.slice(0, defaultIndex);
|
|
let after = mountedTabs.slice(defaultIndex);
|
|
|
|
let next = [...after, ...before].find((tab) =>
|
|
focusableTabs.includes(tab)
|
|
);
|
|
if (!next) return;
|
|
|
|
selectedIndex = mountedTabs.indexOf(next);
|
|
}
|
|
});
|
|
|
|
$: slotProps = { selectedIndex };
|
|
</script>
|
|
|
|
<Render
|
|
{...$$restProps}
|
|
{as}
|
|
{slotProps}
|
|
use={[...use, forwardEvents]}
|
|
name={"TabGroup"}
|
|
>
|
|
<slot {...slotProps} />
|
|
</Render>
|