UI: always prefer session storage on Tabs

This commit is contained in:
Fuma Nama
2025-11-29 11:56:16 +08:00
parent dadefbf421
commit 12d3f78dce
4 changed files with 33 additions and 46 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-ui': patch
---
Tabs: always prefer session storage
@@ -74,8 +74,10 @@ id.
### Persistent
You can enable persistent by passing a `persist` property. The value will be
stored in `localStorage`, with its id as the key.
When `groupId` is specified, their value will be temporarily stored in `sessionStorage`.
To persist the value, you can specify a `persist` prop. The value will be
stored in `localStorage`, with its group ID as storage key.
```mdx
<Tabs groupId="language" items={['Javascript', 'Rust']} persist>
@@ -84,8 +86,6 @@ stored in `localStorage`, with its id as the key.
</Tabs>
```
> Persistent only works if you have passed an `id`.
### Default Value
Set a default value by passing `defaultIndex`.
@@ -180,7 +180,7 @@ Fumadocs UI does this with a grid system:
minmax(min-content, 1fr);
--fd-docs-row-1: var(--fd-banner-height, 0px);
--fd-docs-row-2: calc(var(--fd-docs-row-1) + var(--fd-header-height) 0px);
--fd-docs-row-2: calc(var(--fd-docs-row-1) + var(--fd-header-height));
--fd-docs-row-3: calc(var(--fd-docs-row-2) + var(--fd-toc-popover-height));
--fd-sidebar-col: var(--fd-sidebar-width);
--fd-page-col: calc(
+23 -41
View File
@@ -3,7 +3,7 @@
import {
type ComponentProps,
createContext,
useContext,
use,
useEffectEvent,
useLayoutEffect,
useMemo,
@@ -14,21 +14,7 @@ import * as Primitive from '@radix-ui/react-tabs';
import { mergeRefs } from '@/utils/merge-refs';
type ChangeListener = (v: string) => void;
const listeners = new Map<string, ChangeListener[]>();
function addChangeListener(id: string, listener: ChangeListener): void {
const list = listeners.get(id) ?? [];
list.push(listener);
listeners.set(id, list);
}
function removeChangeListener(id: string, listener: ChangeListener): void {
const list = listeners.get(id) ?? [];
listeners.set(
id,
list.filter((item) => item !== listener),
);
}
const listeners = new Map<string, Set<ChangeListener>>();
export interface TabsProps extends ComponentProps<typeof Primitive.Tabs> {
/**
@@ -52,7 +38,7 @@ const TabsContext = createContext<{
} | null>(null);
function useTabContext() {
const ctx = useContext(TabsContext);
const ctx = use(TabsContext);
if (!ctx) throw new Error('You must wrap your component in <Tabs>');
return ctx;
}
@@ -61,9 +47,6 @@ export const TabsList = Primitive.TabsList;
export const TabsTrigger = Primitive.TabsTrigger;
/**
* @internal You better not use it
*/
export function Tabs({
ref,
groupId,
@@ -75,27 +58,27 @@ export function Tabs({
...props
}: TabsProps) {
const tabsRef = useRef<HTMLDivElement>(null);
const valueToIdMap = useMemo(() => new Map<string, string>(), []);
const [value, setValue] =
_value === undefined
? // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled
useState(defaultValue)
: [_value, _onValueChange ?? (() => undefined)];
const onChange = useEffectEvent((v: string) => setValue(v));
const valueToIdMap = useMemo(() => new Map<string, string>(), []);
: // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled
[_value, useEffectEvent((v: string) => _onValueChange?.(v))];
useLayoutEffect(() => {
if (!groupId) return;
const previous = persist
? localStorage.getItem(groupId)
: sessionStorage.getItem(groupId);
let previous = sessionStorage.getItem(groupId);
if (persist) previous ??= localStorage.getItem(groupId);
if (previous) setValue(previous);
if (previous) onChange(previous);
addChangeListener(groupId, onChange);
const groupListeners = listeners.get(groupId) ?? new Set();
groupListeners.add(setValue);
listeners.set(groupId, groupListeners);
return () => {
removeChangeListener(groupId, onChange);
groupListeners.delete(setValue);
};
}, [groupId, persist]);
}, [groupId, persist, setValue]);
useLayoutEffect(() => {
const hash = window.location.hash.slice(1);
@@ -103,12 +86,12 @@ export function Tabs({
for (const [value, id] of valueToIdMap.entries()) {
if (id === hash) {
onChange(value);
setValue(value);
tabsRef.current?.scrollIntoView();
break;
}
}
}, [valueToIdMap]);
}, [setValue, valueToIdMap]);
return (
<Primitive.Tabs
@@ -124,23 +107,22 @@ export function Tabs({
}
if (groupId) {
listeners.get(groupId)?.forEach((item) => {
item(v);
});
const groupListeners = listeners.get(groupId);
if (groupListeners) {
for (const listener of groupListeners) listener(v);
}
sessionStorage.setItem(groupId, v);
if (persist) localStorage.setItem(groupId, v);
else sessionStorage.setItem(groupId, v);
} else {
setValue(v);
}
}}
{...props}
>
<TabsContext.Provider
value={useMemo(() => ({ valueToIdMap }), [valueToIdMap])}
>
<TabsContext value={useMemo(() => ({ valueToIdMap }), [valueToIdMap])}>
{props.children}
</TabsContext.Provider>
</TabsContext>
</Primitive.Tabs>
);
}