react-multi-tab 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -0
- package/dist/adapters/react-router.cjs +41 -0
- package/dist/adapters/react-router.cjs.map +1 -0
- package/dist/adapters/react-router.d.cts +33 -0
- package/dist/adapters/react-router.d.ts +33 -0
- package/dist/adapters/react-router.mjs +39 -0
- package/dist/adapters/react-router.mjs.map +1 -0
- package/dist/index.cjs +632 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +276 -0
- package/dist/index.d.ts +276 -0
- package/dist/index.mjs +618 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types-Df_5I7eH.d.cts +103 -0
- package/dist/types-Df_5I7eH.d.ts +103 -0
- package/package.json +117 -0
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# react-multi-tab
|
|
2
|
+
|
|
3
|
+
A headless, accessible, router-agnostic, and fully type-safe multi-tab component library for React.
|
|
4
|
+
|
|
5
|
+
[](https://npmjs.org/package/react-multi-tab)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Agnostic & Pluggable:** Works independently of any bundler or router. Includes adapters for memory, URL Search Params, and `react-router-dom`.
|
|
11
|
+
- **Headless & Accessible:** Follows the WAI-ARIA Tabs pattern. Complete keyboard navigation (`Arrow keys`, `Home`, `End`) out of the box. You control the styling.
|
|
12
|
+
- **TypeScript Generics:** Fully type-safe context, components, and hooks. Never use `any` again.
|
|
13
|
+
- **State Preservation:** Keeps tab content mounted when inactive (via `hidden` attribute) to preserve form states and scroll positions.
|
|
14
|
+
|
|
15
|
+
## 🚀 Performance & Developer Experience (DX)
|
|
16
|
+
|
|
17
|
+
We have built the core state-management of `react-multi-tab` to meet the highest performance and Developer Experience (DX) standards.
|
|
18
|
+
|
|
19
|
+
### 1. `useSyncExternalStore` (The Performance Boost)
|
|
20
|
+
We stripped the large object out of React Context and replaced it with a custom Vanilla JS store (`createMultiTabStore`).
|
|
21
|
+
By utilizing React 18's `useSyncExternalStore`, components like `useTabData` now strictly subscribe to only the data they care about.
|
|
22
|
+
|
|
23
|
+
> **Why it matters:** Typing in an input field inside a specific tab will **NO LONGER** trigger a re-render across the entire Tab system. Only the component that called `useTabData` for that specific tab will update!
|
|
24
|
+
|
|
25
|
+
### 2. `TabInstanceContext` (The DX Boost)
|
|
26
|
+
Developers no longer need to manually pass down the cryptic `instanceId` to every page component.
|
|
27
|
+
Because `<TabPanels />` implicitly wraps your components with `<TabContent>`, it invisibly provides the context to all descendants.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// Inside any page (e.g., A-Page.tsx)
|
|
31
|
+
const [data, setData] = useTabData<APageData>(); // Boom! Magic.
|
|
32
|
+
```
|
|
33
|
+
> `useTabData` automatically finds its own tab's context. If no context is found, it safely falls back to the globally active tab.
|
|
34
|
+
|
|
35
|
+
### 3. Built for Redux/Zustand Users
|
|
36
|
+
For teams that prefer keeping form data in a global Redux slice instead of our internal data store, we introduced the `useTabInstanceId()` hook.
|
|
37
|
+
```tsx
|
|
38
|
+
import { useTabInstanceId } from 'react-multi-tab';
|
|
39
|
+
|
|
40
|
+
function MyReduxPage() {
|
|
41
|
+
const tabId = useTabInstanceId(); // Seamlessly gets the ID!
|
|
42
|
+
|
|
43
|
+
const handleChange = () => {
|
|
44
|
+
dispatch(updateFormSlice({ tabId, data: '...' }));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 4. Smart Tab History
|
|
50
|
+
When a tab is closed, the library remembers the exact history of your active tabs and gracefully falls back to the **previously active** tab instead of abruptly jumping to the end of the list. Exactly like VS Code!
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install react-multi-tab
|
|
56
|
+
# or
|
|
57
|
+
yarn add react-multi-tab
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Basic Usage
|
|
61
|
+
|
|
62
|
+
Here is a full example showing how to build a tabbed layout using the vanilla URL `searchParamsAdapter` and the new explicit page registry.
|
|
63
|
+
|
|
64
|
+
### 1. Define Your Pages & Registry
|
|
65
|
+
|
|
66
|
+
Create your page components and register them explicitly.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
// src/registry.ts
|
|
70
|
+
import { createPageRegistry, useTabData } from 'react-multi-tab';
|
|
71
|
+
|
|
72
|
+
interface DashboardData { filter?: string; }
|
|
73
|
+
|
|
74
|
+
function Dashboard({ instanceId }: { instanceId: string }) {
|
|
75
|
+
// 100% Type-safe!
|
|
76
|
+
const [data, setData] = useTabData<DashboardData>();
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<h2>Dashboard</h2>
|
|
81
|
+
<p>Filter: {data.filter ?? 'None'}</p>
|
|
82
|
+
<button onClick={() => setData({ filter: 'Active' })}>Set Filter</button>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Settings() {
|
|
88
|
+
return <h2>Settings</h2>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create a bundler-agnostic registry
|
|
92
|
+
export const registry = createPageRegistry([
|
|
93
|
+
{ id: 'dashboard', label: 'Dashboard', component: Dashboard },
|
|
94
|
+
{ id: 'settings', label: 'Settings', component: Settings },
|
|
95
|
+
]);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. Wrap with Provider & Build the Layout
|
|
99
|
+
|
|
100
|
+
Use the provided headless components to build your accessible tab interface. They include all necessary ARIA attributes and keyboard events.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
// src/App.tsx
|
|
104
|
+
import {
|
|
105
|
+
MultiTabProvider,
|
|
106
|
+
searchParamsAdapter,
|
|
107
|
+
TabList,
|
|
108
|
+
TabTrigger,
|
|
109
|
+
TabCloseButton,
|
|
110
|
+
TabPanels,
|
|
111
|
+
useMultiTab
|
|
112
|
+
} from 'react-multi-tab';
|
|
113
|
+
import { registry } from './registry';
|
|
114
|
+
|
|
115
|
+
function MainLayout() {
|
|
116
|
+
const { tabs, openTab } = useMultiTab();
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div style={{ display: 'flex' }}>
|
|
120
|
+
{/* Sidebar / Menu */}
|
|
121
|
+
<nav style={{ width: 200 }}>
|
|
122
|
+
<button onClick={() => openTab('dashboard')}>Open Dashboard</button>
|
|
123
|
+
<button onClick={() => openTab('settings')}>Open Settings</button>
|
|
124
|
+
</nav>
|
|
125
|
+
|
|
126
|
+
<div style={{ flex: 1 }}>
|
|
127
|
+
{/* Accessible Tab List */}
|
|
128
|
+
<TabList aria-label="My Application Tabs">
|
|
129
|
+
{tabs.map((tab) => (
|
|
130
|
+
<TabTrigger key={tab.instanceId} instanceId={tab.instanceId}>
|
|
131
|
+
{tab.label}
|
|
132
|
+
<TabCloseButton instanceId={tab.instanceId} />
|
|
133
|
+
</TabTrigger>
|
|
134
|
+
))}
|
|
135
|
+
</TabList>
|
|
136
|
+
|
|
137
|
+
{/* Renders all active and hidden tab panels */}
|
|
138
|
+
<TabPanels />
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default function App() {
|
|
145
|
+
return (
|
|
146
|
+
<MultiTabProvider
|
|
147
|
+
registry={registry}
|
|
148
|
+
adapter={searchParamsAdapter()} // Syncs active tabs to the browser URL
|
|
149
|
+
>
|
|
150
|
+
<MainLayout />
|
|
151
|
+
</MultiTabProvider>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Running the Playground
|
|
157
|
+
|
|
158
|
+
You can easily test the components locally using the built-in Playground!
|
|
159
|
+
|
|
160
|
+
1. Go to the project root directory.
|
|
161
|
+
2. Run the playground command:
|
|
162
|
+
```bash
|
|
163
|
+
yarn run dev:demo
|
|
164
|
+
```
|
|
165
|
+
3. Open `http://localhost:3000` to interact with the demo.
|
|
166
|
+
|
|
167
|
+
## Advanced Features
|
|
168
|
+
|
|
169
|
+
### React Router Integration
|
|
170
|
+
|
|
171
|
+
If you want to sync your tab state with React Router, you can use the optional adapter.
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
import { MultiTabProvider } from 'react-multi-tab';
|
|
175
|
+
import { useReactRouterAdapter } from 'react-multi-tab/adapters/react-router';
|
|
176
|
+
|
|
177
|
+
function App() {
|
|
178
|
+
const routerAdapter = useReactRouterAdapter();
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<MultiTabProvider registry={registry} adapter={routerAdapter}>
|
|
182
|
+
<MainLayout />
|
|
183
|
+
</MultiTabProvider>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT © Arif GEVENCİ
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var reactRouterDom = require('react-router-dom');
|
|
5
|
+
|
|
6
|
+
// src/adapters/react-router.ts
|
|
7
|
+
function useReactRouterAdapter(options) {
|
|
8
|
+
const [, setSearchParams] = reactRouterDom.useSearchParams();
|
|
9
|
+
const setParamsRef = react.useRef(setSearchParams);
|
|
10
|
+
setParamsRef.current = setSearchParams;
|
|
11
|
+
const tabsKey = options?.tabsParam ?? "tabs";
|
|
12
|
+
const activeKey = options?.activeParam ?? "active";
|
|
13
|
+
return react.useMemo(
|
|
14
|
+
() => ({
|
|
15
|
+
read() {
|
|
16
|
+
const params = new URLSearchParams(window.location.search);
|
|
17
|
+
const tabsStr = params.get(tabsKey);
|
|
18
|
+
const activeTab = params.get(activeKey);
|
|
19
|
+
if (!tabsStr) return null;
|
|
20
|
+
return { tabs: tabsStr.split(",").filter(Boolean), activeTab };
|
|
21
|
+
},
|
|
22
|
+
write(tabs, activeTabId) {
|
|
23
|
+
const newParams = {};
|
|
24
|
+
if (tabs.length > 0) {
|
|
25
|
+
newParams[tabsKey] = tabs.map((t) => t.instanceId).join(",");
|
|
26
|
+
if (activeTabId) newParams[activeKey] = activeTabId;
|
|
27
|
+
}
|
|
28
|
+
setParamsRef.current(newParams, { replace: true });
|
|
29
|
+
},
|
|
30
|
+
subscribe(callback) {
|
|
31
|
+
window.addEventListener("popstate", callback);
|
|
32
|
+
return () => window.removeEventListener("popstate", callback);
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
[tabsKey, activeKey]
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
exports.useReactRouterAdapter = useReactRouterAdapter;
|
|
40
|
+
//# sourceMappingURL=react-router.cjs.map
|
|
41
|
+
//# sourceMappingURL=react-router.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/react-router.ts"],"names":["useSearchParams","useRef","useMemo"],"mappings":";;;;;;AAgCO,SAAS,sBACd,OAAA,EACY;AACZ,EAAA,MAAM,GAAG,eAAe,CAAA,GAAIA,8BAAA,EAAgB;AAG5C,EAAA,MAAM,YAAA,GAAeC,aAAO,eAAe,CAAA;AAC3C,EAAA,YAAA,CAAa,OAAA,GAAU,eAAA;AAEvB,EAAA,MAAM,OAAA,GAAU,SAAS,SAAA,IAAa,MAAA;AACtC,EAAA,MAAM,SAAA,GAAY,SAAS,WAAA,IAAe,QAAA;AAE1C,EAAA,OAAOC,aAAA;AAAA,IACL,OAAO;AAAA,MACL,IAAA,GAAO;AACL,QAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,MAAA,CAAO,SAAS,MAAM,CAAA;AACzD,QAAA,MAAM,OAAA,GAAU,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA;AAClC,QAAA,MAAM,SAAA,GAAY,MAAA,CAAO,GAAA,CAAI,SAAS,CAAA;AAEtC,QAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AACrB,QAAA,OAAO,EAAE,MAAM,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,EAAG,SAAA,EAAU;AAAA,MAC/D,CAAA;AAAA,MAEA,KAAA,CAAM,MAAqB,WAAA,EAA4B;AACrD,QAAA,MAAM,YAAoC,EAAC;AAC3C,QAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AACnB,UAAA,SAAA,CAAU,OAAO,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,UAAU,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAC3D,UAAA,IAAI,WAAA,EAAa,SAAA,CAAU,SAAS,CAAA,GAAI,WAAA;AAAA,QAC1C;AACA,QAAA,YAAA,CAAa,OAAA,CAAQ,SAAA,EAAW,EAAE,OAAA,EAAS,MAAM,CAAA;AAAA,MACnD,CAAA;AAAA,MAEA,UAAU,QAAA,EAAsB;AAC9B,QAAA,MAAA,CAAO,gBAAA,CAAiB,YAAY,QAAQ,CAAA;AAC5C,QAAA,OAAO,MAAM,MAAA,CAAO,mBAAA,CAAoB,UAAA,EAAY,QAAQ,CAAA;AAAA,MAC9D;AAAA,KACF,CAAA;AAAA,IACA,CAAC,SAAS,SAAS;AAAA,GACrB;AACF","file":"react-router.cjs","sourcesContent":["import { useMemo, useRef } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport type { TabInstance, URLAdapter } from \"../types\";\n\n/** Options for {@link useReactRouterAdapter}. */\nexport interface ReactRouterAdapterOptions {\n /** Query-string key for tab instance IDs. @default \"tabs\" */\n tabsParam?: string;\n /** Query-string key for the active tab. @default \"active\" */\n activeParam?: string;\n}\n\n/**\n * React Router v6+ adapter (hook).\n *\n * Must be called **inside** a `<BrowserRouter>` or equivalent.\n * Returns a stable {@link URLAdapter} reference.\n *\n * @example\n * ```tsx\n * import { useReactRouterAdapter } from 'react-multi-tab/adapters/react-router';\n *\n * function App() {\n * const adapter = useReactRouterAdapter();\n * return (\n * <MultiTabProvider adapter={adapter} registry={registry}>\n * ...\n * </MultiTabProvider>\n * );\n * }\n * ```\n */\nexport function useReactRouterAdapter(\n options?: ReactRouterAdapterOptions\n): URLAdapter {\n const [, setSearchParams] = useSearchParams();\n\n // Keep a stable ref so the adapter object identity doesn't change.\n const setParamsRef = useRef(setSearchParams);\n setParamsRef.current = setSearchParams;\n\n const tabsKey = options?.tabsParam ?? \"tabs\";\n const activeKey = options?.activeParam ?? \"active\";\n\n return useMemo<URLAdapter>(\n () => ({\n read() {\n const params = new URLSearchParams(window.location.search);\n const tabsStr = params.get(tabsKey);\n const activeTab = params.get(activeKey);\n\n if (!tabsStr) return null;\n return { tabs: tabsStr.split(\",\").filter(Boolean), activeTab };\n },\n\n write(tabs: TabInstance[], activeTabId: string | null) {\n const newParams: Record<string, string> = {};\n if (tabs.length > 0) {\n newParams[tabsKey] = tabs.map((t) => t.instanceId).join(\",\");\n if (activeTabId) newParams[activeKey] = activeTabId;\n }\n setParamsRef.current(newParams, { replace: true });\n },\n\n subscribe(callback: () => void) {\n window.addEventListener(\"popstate\", callback);\n return () => window.removeEventListener(\"popstate\", callback);\n },\n }),\n [tabsKey, activeKey]\n );\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { U as URLAdapter } from '../types-Df_5I7eH.cjs';
|
|
2
|
+
import 'react';
|
|
3
|
+
|
|
4
|
+
/** Options for {@link useReactRouterAdapter}. */
|
|
5
|
+
interface ReactRouterAdapterOptions {
|
|
6
|
+
/** Query-string key for tab instance IDs. @default "tabs" */
|
|
7
|
+
tabsParam?: string;
|
|
8
|
+
/** Query-string key for the active tab. @default "active" */
|
|
9
|
+
activeParam?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* React Router v6+ adapter (hook).
|
|
13
|
+
*
|
|
14
|
+
* Must be called **inside** a `<BrowserRouter>` or equivalent.
|
|
15
|
+
* Returns a stable {@link URLAdapter} reference.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { useReactRouterAdapter } from 'react-multi-tab/adapters/react-router';
|
|
20
|
+
*
|
|
21
|
+
* function App() {
|
|
22
|
+
* const adapter = useReactRouterAdapter();
|
|
23
|
+
* return (
|
|
24
|
+
* <MultiTabProvider adapter={adapter} registry={registry}>
|
|
25
|
+
* ...
|
|
26
|
+
* </MultiTabProvider>
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare function useReactRouterAdapter(options?: ReactRouterAdapterOptions): URLAdapter;
|
|
32
|
+
|
|
33
|
+
export { type ReactRouterAdapterOptions, useReactRouterAdapter };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { U as URLAdapter } from '../types-Df_5I7eH.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
|
|
4
|
+
/** Options for {@link useReactRouterAdapter}. */
|
|
5
|
+
interface ReactRouterAdapterOptions {
|
|
6
|
+
/** Query-string key for tab instance IDs. @default "tabs" */
|
|
7
|
+
tabsParam?: string;
|
|
8
|
+
/** Query-string key for the active tab. @default "active" */
|
|
9
|
+
activeParam?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* React Router v6+ adapter (hook).
|
|
13
|
+
*
|
|
14
|
+
* Must be called **inside** a `<BrowserRouter>` or equivalent.
|
|
15
|
+
* Returns a stable {@link URLAdapter} reference.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { useReactRouterAdapter } from 'react-multi-tab/adapters/react-router';
|
|
20
|
+
*
|
|
21
|
+
* function App() {
|
|
22
|
+
* const adapter = useReactRouterAdapter();
|
|
23
|
+
* return (
|
|
24
|
+
* <MultiTabProvider adapter={adapter} registry={registry}>
|
|
25
|
+
* ...
|
|
26
|
+
* </MultiTabProvider>
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare function useReactRouterAdapter(options?: ReactRouterAdapterOptions): URLAdapter;
|
|
32
|
+
|
|
33
|
+
export { type ReactRouterAdapterOptions, useReactRouterAdapter };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useRef, useMemo } from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
// src/adapters/react-router.ts
|
|
5
|
+
function useReactRouterAdapter(options) {
|
|
6
|
+
const [, setSearchParams] = useSearchParams();
|
|
7
|
+
const setParamsRef = useRef(setSearchParams);
|
|
8
|
+
setParamsRef.current = setSearchParams;
|
|
9
|
+
const tabsKey = options?.tabsParam ?? "tabs";
|
|
10
|
+
const activeKey = options?.activeParam ?? "active";
|
|
11
|
+
return useMemo(
|
|
12
|
+
() => ({
|
|
13
|
+
read() {
|
|
14
|
+
const params = new URLSearchParams(window.location.search);
|
|
15
|
+
const tabsStr = params.get(tabsKey);
|
|
16
|
+
const activeTab = params.get(activeKey);
|
|
17
|
+
if (!tabsStr) return null;
|
|
18
|
+
return { tabs: tabsStr.split(",").filter(Boolean), activeTab };
|
|
19
|
+
},
|
|
20
|
+
write(tabs, activeTabId) {
|
|
21
|
+
const newParams = {};
|
|
22
|
+
if (tabs.length > 0) {
|
|
23
|
+
newParams[tabsKey] = tabs.map((t) => t.instanceId).join(",");
|
|
24
|
+
if (activeTabId) newParams[activeKey] = activeTabId;
|
|
25
|
+
}
|
|
26
|
+
setParamsRef.current(newParams, { replace: true });
|
|
27
|
+
},
|
|
28
|
+
subscribe(callback) {
|
|
29
|
+
window.addEventListener("popstate", callback);
|
|
30
|
+
return () => window.removeEventListener("popstate", callback);
|
|
31
|
+
}
|
|
32
|
+
}),
|
|
33
|
+
[tabsKey, activeKey]
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { useReactRouterAdapter };
|
|
38
|
+
//# sourceMappingURL=react-router.mjs.map
|
|
39
|
+
//# sourceMappingURL=react-router.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/react-router.ts"],"names":[],"mappings":";;;;AAgCO,SAAS,sBACd,OAAA,EACY;AACZ,EAAA,MAAM,GAAG,eAAe,CAAA,GAAI,eAAA,EAAgB;AAG5C,EAAA,MAAM,YAAA,GAAe,OAAO,eAAe,CAAA;AAC3C,EAAA,YAAA,CAAa,OAAA,GAAU,eAAA;AAEvB,EAAA,MAAM,OAAA,GAAU,SAAS,SAAA,IAAa,MAAA;AACtC,EAAA,MAAM,SAAA,GAAY,SAAS,WAAA,IAAe,QAAA;AAE1C,EAAA,OAAO,OAAA;AAAA,IACL,OAAO;AAAA,MACL,IAAA,GAAO;AACL,QAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,MAAA,CAAO,SAAS,MAAM,CAAA;AACzD,QAAA,MAAM,OAAA,GAAU,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA;AAClC,QAAA,MAAM,SAAA,GAAY,MAAA,CAAO,GAAA,CAAI,SAAS,CAAA;AAEtC,QAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AACrB,QAAA,OAAO,EAAE,MAAM,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,EAAG,SAAA,EAAU;AAAA,MAC/D,CAAA;AAAA,MAEA,KAAA,CAAM,MAAqB,WAAA,EAA4B;AACrD,QAAA,MAAM,YAAoC,EAAC;AAC3C,QAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AACnB,UAAA,SAAA,CAAU,OAAO,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,UAAU,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAC3D,UAAA,IAAI,WAAA,EAAa,SAAA,CAAU,SAAS,CAAA,GAAI,WAAA;AAAA,QAC1C;AACA,QAAA,YAAA,CAAa,OAAA,CAAQ,SAAA,EAAW,EAAE,OAAA,EAAS,MAAM,CAAA;AAAA,MACnD,CAAA;AAAA,MAEA,UAAU,QAAA,EAAsB;AAC9B,QAAA,MAAA,CAAO,gBAAA,CAAiB,YAAY,QAAQ,CAAA;AAC5C,QAAA,OAAO,MAAM,MAAA,CAAO,mBAAA,CAAoB,UAAA,EAAY,QAAQ,CAAA;AAAA,MAC9D;AAAA,KACF,CAAA;AAAA,IACA,CAAC,SAAS,SAAS;AAAA,GACrB;AACF","file":"react-router.mjs","sourcesContent":["import { useMemo, useRef } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport type { TabInstance, URLAdapter } from \"../types\";\n\n/** Options for {@link useReactRouterAdapter}. */\nexport interface ReactRouterAdapterOptions {\n /** Query-string key for tab instance IDs. @default \"tabs\" */\n tabsParam?: string;\n /** Query-string key for the active tab. @default \"active\" */\n activeParam?: string;\n}\n\n/**\n * React Router v6+ adapter (hook).\n *\n * Must be called **inside** a `<BrowserRouter>` or equivalent.\n * Returns a stable {@link URLAdapter} reference.\n *\n * @example\n * ```tsx\n * import { useReactRouterAdapter } from 'react-multi-tab/adapters/react-router';\n *\n * function App() {\n * const adapter = useReactRouterAdapter();\n * return (\n * <MultiTabProvider adapter={adapter} registry={registry}>\n * ...\n * </MultiTabProvider>\n * );\n * }\n * ```\n */\nexport function useReactRouterAdapter(\n options?: ReactRouterAdapterOptions\n): URLAdapter {\n const [, setSearchParams] = useSearchParams();\n\n // Keep a stable ref so the adapter object identity doesn't change.\n const setParamsRef = useRef(setSearchParams);\n setParamsRef.current = setSearchParams;\n\n const tabsKey = options?.tabsParam ?? \"tabs\";\n const activeKey = options?.activeParam ?? \"active\";\n\n return useMemo<URLAdapter>(\n () => ({\n read() {\n const params = new URLSearchParams(window.location.search);\n const tabsStr = params.get(tabsKey);\n const activeTab = params.get(activeKey);\n\n if (!tabsStr) return null;\n return { tabs: tabsStr.split(\",\").filter(Boolean), activeTab };\n },\n\n write(tabs: TabInstance[], activeTabId: string | null) {\n const newParams: Record<string, string> = {};\n if (tabs.length > 0) {\n newParams[tabsKey] = tabs.map((t) => t.instanceId).join(\",\");\n if (activeTabId) newParams[activeKey] = activeTabId;\n }\n setParamsRef.current(newParams, { replace: true });\n },\n\n subscribe(callback: () => void) {\n window.addEventListener(\"popstate\", callback);\n return () => window.removeEventListener(\"popstate\", callback);\n },\n }),\n [tabsKey, activeKey]\n );\n}\n"]}
|