Skip to content

Commit ecd061c

Browse files
committed
feat(Tabs): add selected and key
1 parent 07f6ba2 commit ecd061c

File tree

4 files changed

+83
-62
lines changed

4 files changed

+83
-62
lines changed

src/lib/tabs/TabItem.svelte

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,55 +6,59 @@
66
import { getContext } from "svelte";
77
import { tabItem, tabs } from ".";
88
9-
let { children, titleSlot, open = $bindable(false), title = "Tab title", activeClass, inactiveClass, class: className, classes, disabled, tabStyle, ...restProps }: TabitemProps = $props();
9+
let { children, titleSlot, open = $bindable(false), title = "Tab title", key, activeClass, inactiveClass, class: className, classes, disabled, tabStyle, ...restProps }: TabitemProps = $props();
1010
1111
const theme = getTheme("tabItem");
12-
1312
const activeClasses = getContext<string>("activeClasses");
1413
const ctx: TabCtxType = getContext("ctx");
15-
let compoTabStyle = $derived(tabStyle ? tabStyle : ctx.tabStyle || "full");
16-
14+
15+
const compoTabStyle = $derived(tabStyle ?? ctx.tabStyle ?? "full");
1716
const { active, inactive } = $derived(tabs({ tabStyle: compoTabStyle, hasDivider: true }));
1817
19-
// Generate a unique ID for this tab button
2018
const tabId = $props.id();
21-
const self = $state({ id: `tab-${tabId}`, snippet: children });
19+
const tabIdentifier = key ?? tabId;
20+
const self: SelectedTab = { id: tabIdentifier, snippet: children };
21+
22+
const registerTab = getContext<(tab: SelectedTab) => void>("registerTab");
23+
const unregisterTab = getContext<(tabId: string) => void>("unregisterTab");
2224
2325
const updateSingleSelection = useSingleSelection<SelectedTab>((value) => (open = value?.id === self.id));
2426
2527
$effect(() => {
26-
// monitor if open changes out side of that component
2728
updateSingleSelection(open, self);
29+
registerTab?.(self);
30+
31+
return () => {
32+
if (self.id) {
33+
unregisterTab?.(self.id);
34+
}
35+
};
2836
});
2937
3038
const { base, button } = $derived(tabItem({ open, disabled }));
3139
</script>
3240

3341
<li {...restProps} class={base({ class: clsx(theme?.base, className) })} role="presentation">
34-
<button type="button" onclick={() => (open = true)} role="tab" id={self.id} aria-controls={ctx.panelId} aria-selected={open} {disabled} class={button({ class: clsx(open ? (activeClass ?? active({ class: activeClasses })) : (inactiveClass ?? inactive()), theme?.button, classes?.button) })}>
42+
<button
43+
type="button"
44+
onclick={() => (open = true)}
45+
role="tab"
46+
id={self.id}
47+
aria-controls={ctx.panelId}
48+
aria-selected={open}
49+
{disabled}
50+
class={button({
51+
class: clsx(
52+
open ? (activeClass ?? active({ class: activeClasses })) : (inactiveClass ?? inactive()),
53+
theme?.button,
54+
classes?.button
55+
)
56+
})}
57+
>
3558
{#if titleSlot}
3659
{@render titleSlot()}
3760
{:else}
3861
{title}
3962
{/if}
4063
</button>
41-
</li>
42-
43-
<!--
44-
@component
45-
[Go to docs](https://flowbite-svelte.com/)
46-
## Type
47-
[TabitemProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#L1729)
48-
## Props
49-
@prop children
50-
@prop titleSlot
51-
@prop open = $bindable(false)
52-
@prop title = "Tab title"
53-
@prop activeClass
54-
@prop inactiveClass
55-
@prop class: className
56-
@prop classes
57-
@prop disabled
58-
@prop tabStyle
59-
@prop ...restProps
60-
-->
64+
</li>

src/lib/tabs/Tabs.svelte

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,66 @@
33
import type { SelectedTab, TabCtxType, TabsProps } from "$lib/types";
44
import { createSingleSelectionContext, useSingleSelection } from "$lib/utils/singleselection.svelte";
55
import clsx from "clsx";
6-
import { setContext, type Snippet } from "svelte";
6+
import { setContext } from "svelte";
77
import { tabs } from ".";
88
9-
let { children, tabStyle = "none", ulClass, contentClass, divider = true, class: className, classes, ...restProps }: TabsProps = $props();
9+
let { children, selected = $bindable(), tabStyle = "none", ulClass, contentClass, divider = true, class: className, classes, ...restProps }: TabsProps = $props();
1010
1111
if (classes?.active) {
1212
setContext("activeClasses", classes.active);
1313
}
1414
15-
// base, content, divider, active, inactive
1615
warnThemeDeprecation("Tabs", { ulClass, contentClass }, { ulClass: "class", contentClass: "content" });
17-
const styling = $derived(classes ?? { content: contentClass });
18-
16+
1917
const theme = getTheme("tabs");
20-
18+
const styling = $derived(classes ?? { content: contentClass });
2119
const { base, content, divider: dividerClass } = $derived(tabs({ tabStyle, hasDivider: divider }));
2220
23-
// Generate a unique ID for the tab panel
2421
const uuid = $props.id();
2522
const panelId = `tab-panel-${uuid}`;
26-
2723
const ctx: TabCtxType = $state({ tabStyle, panelId });
28-
29-
let dividerBool = $derived(["full", "pill"].includes(tabStyle) ? false : divider);
24+
const dividerBool = $derived(["full", "pill"].includes(tabStyle) ? false : divider);
3025
3126
setContext("ctx", ctx);
32-
3327
createSingleSelectionContext<SelectedTab>();
3428
35-
let selected: SelectedTab = $state({});
36-
useSingleSelection<SelectedTab>((v) => (selected = v ?? {}));
29+
const tabRegistry = $state(new Map<string, SelectedTab>());
30+
let selectedTab: SelectedTab = $state({});
31+
32+
const updateSelection = useSingleSelection<SelectedTab>((v) => {
33+
selectedTab = v ?? {};
34+
selected = v?.id;
35+
});
36+
37+
// Handle external changes to selected
38+
$effect(() => {
39+
if (selected && selected !== selectedTab.id) {
40+
const targetTab = tabRegistry.get(selected);
41+
if (targetTab) {
42+
updateSelection(true, targetTab);
43+
}
44+
}
45+
});
46+
47+
// Auto-select logic
48+
$effect(() => {
49+
if (tabRegistry.size > 0 && !selectedTab.id) {
50+
const targetTab = selected ? tabRegistry.get(selected) : tabRegistry.values().next().value;
51+
if (targetTab) {
52+
updateSelection(true, targetTab);
53+
}
54+
}
55+
});
56+
57+
setContext("registerTab", (tabData: SelectedTab) => {
58+
if (tabData.id) {
59+
tabRegistry.set(tabData.id, tabData);
60+
}
61+
});
62+
63+
setContext("unregisterTab", (tabId: string) => {
64+
tabRegistry.delete(tabId);
65+
});
3766
</script>
3867

3968
<ul role="tablist" {...restProps} class={base({ class: clsx(theme?.base, className ?? ulClass) })}>
@@ -42,22 +71,7 @@
4271
{#if dividerBool}
4372
<div class={dividerClass({ class: clsx(theme?.divider, classes?.divider) })}></div>
4473
{/if}
45-
<div id={panelId} class={content({ class: clsx(theme?.content, styling.content) })} role="tabpanel" aria-labelledby={selected.id}>
46-
{@render selected.snippet?.()}
47-
</div>
48-
49-
<!--
50-
@component
51-
[Go to docs](https://flowbite-svelte.com/)
52-
## Type
53-
[TabsProps](https://github.com/themesberg/flowbite-svelte/blob/main/src/lib/types.ts#L1721)
54-
## Props
55-
@prop children
56-
@prop tabStyle = "none"
57-
@prop ulClass
58-
@prop contentClass
59-
@prop divider = true
60-
@prop class: className
61-
@prop classes
62-
@prop ...restProps
63-
-->
74+
75+
<div id={panelId} class={content({ class: clsx(theme?.content, styling.content) })} role="tabpanel" aria-labelledby={selectedTab.id}>
76+
{@render selectedTab.snippet?.()}
77+
</div>

src/lib/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1720,6 +1720,7 @@ export interface TableSearchProps extends TableSearchVariants, HTMLTableAttribut
17201720
// tabs
17211721
export interface TabsProps extends TabsVaraints, HTMLAttributes<HTMLUListElement> {
17221722
children: Snippet;
1723+
selected?: string;
17231724
tabStyle?: TabsVaraints["tabStyle"];
17241725
ulClass?: ClassValue;
17251726
contentClass?: ClassValue;
@@ -1730,7 +1731,8 @@ export interface TabitemProps extends TabItemVariants, HTMLLiAttributes {
17301731
children?: Snippet;
17311732
titleSlot?: Snippet;
17321733
open?: boolean;
1733-
title?: string;
1734+
title?: string; // for UI label
1735+
key?: string; // for identifier
17341736
activeClass?: ClassValue;
17351737
inactiveClass?: ClassValue;
17361738
disabled?: boolean;

src/lib/utils/singleselection.svelte.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ function setSelected(context, open, value) {
4141
*/
4242
export function useSingleSelection(callback) {
4343
const context = getContext(SINGLE_SELECTION_KEY) ?? createSingleSelectionContext(false);
44-
if (!Object.prototype.hasOwnProperty.call(context, "value")) return () => context; // non-reactive context, do nothing
44+
45+
if (!context.hasOwnProperty?.("value")) return () => context;
4546

4647
$effect(() => {
4748
if (context.value !== null) callback(context.value);

0 commit comments

Comments
 (0)