Files
svelte-headlessui/src/lib/components/tabs/TabGroup.svelte
Ryan Gossiaux 9772e49054 Fix active tab selection on mount
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.
2021-12-29 10:29:26 -10:00

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>