Initial commit with files
Still need to fix the imports
This commit is contained in:
98
src/lib/components/tabs/Tab.svelte
Normal file
98
src/lib/components/tabs/Tab.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { Focus, focusIn } from "./focus-management";
|
||||
import { Keys } from "./keyboard";
|
||||
import { match } from "./match";
|
||||
|
||||
import { useTabsContext } from "./TabGroup.svelte";
|
||||
import { useId } from "./use-id";
|
||||
|
||||
export let disabled = false;
|
||||
|
||||
let api = useTabsContext("Tab");
|
||||
let id = `headlessui-tabs-tab-${useId()}`;
|
||||
let tabRef = null;
|
||||
|
||||
onMount(() => {
|
||||
$api.registerTab(tabRef);
|
||||
return () => $api.unregisterTab(tabRef);
|
||||
});
|
||||
|
||||
$: myIndex = $api.tabs.indexOf(tabRef);
|
||||
$: selected = myIndex === $api.selectedIndex;
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
let list = $api?.tabs.filter(Boolean) as HTMLElement[];
|
||||
|
||||
if (event.key === Keys.Space || event.key === Keys.Enter) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
$api?.setSelectedIndex(myIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case Keys.Home:
|
||||
case Keys.PageUp:
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return focusIn(list, Focus.First);
|
||||
|
||||
case Keys.End:
|
||||
case Keys.PageDown:
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return focusIn(list, Focus.Last);
|
||||
}
|
||||
|
||||
return match($api.orientation, {
|
||||
vertical() {
|
||||
if (event.key === Keys.ArrowUp)
|
||||
return focusIn(list, Focus.Previous | Focus.WrapAround);
|
||||
if (event.key === Keys.ArrowDown)
|
||||
return focusIn(list, Focus.Next | Focus.WrapAround);
|
||||
return;
|
||||
},
|
||||
horizontal() {
|
||||
if (event.key === Keys.ArrowLeft)
|
||||
return focusIn(list, Focus.Previous | Focus.WrapAround);
|
||||
if (event.key === Keys.ArrowRight)
|
||||
return focusIn(list, Focus.Next | Focus.WrapAround);
|
||||
return;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleFocus() {
|
||||
tabRef?.focus();
|
||||
}
|
||||
|
||||
function handleSelection() {
|
||||
if (disabled) return;
|
||||
|
||||
tabRef?.focus();
|
||||
$api?.setSelectedIndex(myIndex);
|
||||
}
|
||||
|
||||
$: propsWeControl = {
|
||||
id,
|
||||
role: "tab",
|
||||
"aria-controls": $api.panels[myIndex]?.id,
|
||||
"aria-selected": selected,
|
||||
tabIndex: selected ? 0 : -1,
|
||||
disabled: disabled ? true : undefined,
|
||||
};
|
||||
</script>
|
||||
|
||||
<button
|
||||
{...{ ...$$restProps, ...propsWeControl }}
|
||||
bind:this={tabRef}
|
||||
on:keydown={handleKeyDown}
|
||||
on:click={handleSelection}
|
||||
on:focus={$api.activation === "manual" ? handleFocus : handleSelection}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
122
src/lib/components/tabs/TabGroup.svelte
Normal file
122
src/lib/components/tabs/TabGroup.svelte
Normal file
@@ -0,0 +1,122 @@
|
||||
<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: (HTMLElement | null)[];
|
||||
|
||||
// State mutators
|
||||
setSelectedIndex(index: number): void;
|
||||
registerTab(tab: HTMLElement | null): void;
|
||||
unregisterTab(tab: HTMLElement | null): void;
|
||||
registerPanel(panel: HTMLElement | null): void;
|
||||
unregisterPanel(panel: HTMLElement | null): void;
|
||||
};
|
||||
|
||||
const TABS_CONTEXT_NAME = "TabsContext";
|
||||
|
||||
export function useTabsContext(
|
||||
component: string
|
||||
): Writable<StateDefinition | undefined> {
|
||||
let context: Writable<StateDefinition | undefined> | 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 | undefined> = writable();
|
||||
setContext(TABS_CONTEXT_NAME, api);
|
||||
|
||||
$: api.set({
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
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>
|
||||
13
src/lib/components/tabs/TabList.svelte
Normal file
13
src/lib/components/tabs/TabList.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { useTabsContext } from "./TabGroup.svelte";
|
||||
|
||||
let api = useTabsContext("TabList");
|
||||
$: propsWeControl = {
|
||||
role: "tablist",
|
||||
"aria-orientation": $api.orientation,
|
||||
};
|
||||
</script>
|
||||
|
||||
<div {...{ ...$$restProps, ...propsWeControl }}>
|
||||
<slot selectedIndex={$api.selectedIndex} />
|
||||
</div>
|
||||
30
src/lib/components/tabs/TabPanel.svelte
Normal file
30
src/lib/components/tabs/TabPanel.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { useTabsContext } from "./TabGroup.svelte";
|
||||
import { useId } from "./use-id";
|
||||
|
||||
let api = useTabsContext("TabPanel");
|
||||
let id = `headlessui-tabs-panel-${useId()}`;
|
||||
let panelRef = null;
|
||||
|
||||
onMount(() => {
|
||||
$api.registerPanel(panelRef);
|
||||
return () => $api.unregisterPanel(panelRef);
|
||||
});
|
||||
|
||||
$: myIndex = $api.panels.indexOf(panelRef);
|
||||
$: selected = myIndex === $api.selectedIndex;
|
||||
|
||||
$: propsWeControl = {
|
||||
id,
|
||||
role: "tabpanel",
|
||||
"aria-labelledby": $api.tabs[myIndex]?.id,
|
||||
tabIndex: selected ? 0 : -1,
|
||||
};
|
||||
</script>
|
||||
|
||||
<div {...{ ...$$restProps, ...propsWeControl }} bind:this={panelRef}>
|
||||
{#if selected}
|
||||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
9
src/lib/components/tabs/TabPanels.svelte
Normal file
9
src/lib/components/tabs/TabPanels.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { useTabsContext } from "./TabGroup.svelte";
|
||||
|
||||
let api = useTabsContext("TabPanels");
|
||||
</script>
|
||||
|
||||
<div {...$$restProps}>
|
||||
<slot selectedIndex={$api.selectedIndex} />
|
||||
</div>
|
||||
Reference in New Issue
Block a user