Skip to content

Commit 33f2cde

Browse files
committed
Fix multiple tabs handling
1 parent 2a98582 commit 33f2cde

File tree

2 files changed

+94
-5
lines changed

2 files changed

+94
-5
lines changed

β€Žpackages/components/src/components/tabs/index.client.tsxβ€Ž

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,12 @@ export interface TabsProps
4848
* @default "tab"
4949
*/
5050
searchParamKey?: string;
51-
/** LocalStorage key for persisting the selected tab. */
52-
storageKey?: string;
51+
/**
52+
* LocalStorage key for persisting the selected tab.
53+
* Defaults to `tabs-${id}` if not provided.
54+
* Set to `null` to disable localStorage persistence.
55+
*/
56+
storageKey?: string | null;
5357
/** Tabs CSS class name. */
5458
className?: TabListProps['className'];
5559
/** Tab CSS class name. */
@@ -69,7 +73,9 @@ export const Tabs = ({
6973
}: TabsProps) => {
7074
const id = useId();
7175

72-
storageKey ??= `tabs-${id}`;
76+
if (storageKey === undefined) {
77+
storageKey = `tabs-${id}`;
78+
}
7379

7480
let [selectedIndex, setSelectedIndex] = useState<number>(defaultIndex);
7581
if (_selectedIndex !== undefined) {
@@ -104,7 +110,23 @@ export const Tabs = ({
104110

105111
if (searchParamKey) {
106112
const searchParams = new URLSearchParams(window.location.search);
107-
searchParams.set(searchParamKey, getTabKey(items, index, id));
113+
const tabKeys = new Set(searchParams.getAll(searchParamKey));
114+
115+
// we remove only tabs from this list from search params
116+
for (let i = 0; i < items.length; i++) {
117+
const key = getTabKey(items, i, id);
118+
tabKeys.delete(key);
119+
}
120+
121+
// we add tabs from outside of this list back
122+
searchParams.delete(searchParamKey);
123+
for (const key of tabKeys) {
124+
searchParams.append(searchParamKey, key);
125+
}
126+
127+
// and finally, we add the clicked tab
128+
searchParams.append(searchParamKey, getTabKey(items, index, id));
129+
108130
window.history.replaceState(
109131
null,
110132
'',
@@ -205,6 +227,14 @@ function useActiveTabFromURL(
205227
tabsInSearchParams.includes(getTabKey(items, index, id)),
206228
);
207229

230+
console.log({
231+
searchParams,
232+
tabIndexFromSearchParams,
233+
tabsInSearchParams,
234+
items,
235+
id,
236+
});
237+
208238
useIsomorphicLayoutEffect(() => {
209239
const tabPanel = hash
210240
? tabPanelsRef.current?.querySelector(`[role=tabpanel]:has([id="${hash}"])`)
@@ -248,7 +278,7 @@ function useActiveTabFromURL(
248278
}
249279

250280
function useActiveTabFromStorage(
251-
storageKey: string,
281+
storageKey: string | null,
252282
items: (TabItem | TabObjectItem)[],
253283
setSelectedIndex: (index: number) => void,
254284
ignoreLocalStorage: boolean,

β€Žpackages/components/src/components/tabs/tabs.stories.tsxβ€Ž

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,3 +483,62 @@ export const ContentInHiddenPanelOpensByHash: Story = {
483483
),
484484
},
485485
};
486+
487+
export const MultipleTabsInParams: Story = {
488+
parameters: {
489+
nextjs: {
490+
appDirectory: true,
491+
navigation: {
492+
query: 'tab=shrimp&tab=brown-rice&tab=mango&tab=ponzu',
493+
},
494+
},
495+
},
496+
render() {
497+
return (
498+
<div className="grid gap-4 lg:grid-cols-2">
499+
<div>
500+
<span className="text-sm font-medium">Protein</span>
501+
<Tabs storageKey={null} items={['Salmon', 'Tuna', 'Tofu', 'Shrimp']}>
502+
<Tabs.Tab>🐟</Tabs.Tab>
503+
<Tabs.Tab>🐠</Tabs.Tab>
504+
<Tabs.Tab>🧈</Tabs.Tab>
505+
<Tabs.Tab>🦐</Tabs.Tab>
506+
</Tabs>
507+
</div>
508+
509+
<div>
510+
<span className="text-sm font-medium">Base</span>
511+
<Tabs
512+
storageKey={null}
513+
items={['White Rice', 'Brown Rice', 'Mixed Greens', 'Zucchini Noodles']}
514+
>
515+
<Tabs.Tab>🍚</Tabs.Tab>
516+
<Tabs.Tab>🍘</Tabs.Tab>
517+
<Tabs.Tab>πŸ₯¬</Tabs.Tab>
518+
<Tabs.Tab>πŸ₯’</Tabs.Tab>
519+
</Tabs>
520+
</div>
521+
522+
<div>
523+
<span className="text-sm font-medium">Toppings</span>
524+
<Tabs storageKey={null} items={['Edamame', 'Avocado', 'Cucumber', 'Mango']}>
525+
<Tabs.Tab>🫘</Tabs.Tab>
526+
<Tabs.Tab>πŸ₯‘</Tabs.Tab>
527+
<Tabs.Tab>πŸ₯’</Tabs.Tab>
528+
<Tabs.Tab>πŸ₯­</Tabs.Tab>
529+
</Tabs>
530+
</div>
531+
532+
<div>
533+
<span className="text-sm font-medium">Sauce</span>
534+
<Tabs storageKey={null} items={['Soy Sauce', 'Ponzu', 'Spicy Mayo', 'Sesame Ginger']}>
535+
<Tabs.Tab>🍢</Tabs.Tab>
536+
<Tabs.Tab>πŸ‹</Tabs.Tab>
537+
<Tabs.Tab>🌢️</Tabs.Tab>
538+
<Tabs.Tab>🫚</Tabs.Tab>
539+
</Tabs>
540+
</div>
541+
</div>
542+
);
543+
},
544+
};

0 commit comments

Comments
Β (0)