diff --git a/packages/react-openapi/src/InteractiveSection.tsx b/packages/react-openapi/src/InteractiveSection.tsx
index 8009b3dd3..2cc6681e3 100644
--- a/packages/react-openapi/src/InteractiveSection.tsx
+++ b/packages/react-openapi/src/InteractiveSection.tsx
@@ -4,6 +4,7 @@ import clsx from 'clsx';
import { useRef, useState } from 'react';
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
import { useDisclosureState } from 'react-stately';
+import { Section, SectionBody, SectionHeader, SectionHeaderContent } from './StaticSection';
interface InteractiveSectionTab {
key: string;
@@ -63,7 +64,7 @@ export function InteractiveSection(props: {
const { isFocusVisible, focusProps } = useFocusRing();
return (
-
{header ? (
-
{
if (toggeable) {
state.toggle();
}
}}
- className={clsx('openapi-section-header', `${className}-header`)}
+ className={className}
>
-
+
{(children || selectedTab?.body) && toggeable ? (
+
) : null}
-
+
) : null}
{(!toggeable || state.isExpanded) && (children || selectedTab?.body) ? (
-
+
{children}
{selectedTab?.body}
-
+
) : null}
{overlay ? (
{overlay}
) : null}
-
+
);
}
diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx
index 8cfba1716..c71a9e7b9 100644
--- a/packages/react-openapi/src/OpenAPICodeSample.tsx
+++ b/packages/react-openapi/src/OpenAPICodeSample.tsx
@@ -1,5 +1,5 @@
-import { InteractiveSection } from './InteractiveSection';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
+import { StaticSection } from './StaticSection';
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
import { stringifyOpenAPI } from './stringifyOpenAPI';
@@ -117,9 +117,9 @@ export function OpenAPICodeSample(props: {
return (
- } className="openapi-codesample">
+ } className="openapi-codesample">
-
+
);
}
diff --git a/packages/react-openapi/src/OpenAPIDisclosure.tsx b/packages/react-openapi/src/OpenAPIDisclosure.tsx
index 19e958ddf..5cd7dad07 100644
--- a/packages/react-openapi/src/OpenAPIDisclosure.tsx
+++ b/packages/react-openapi/src/OpenAPIDisclosure.tsx
@@ -1,3 +1,4 @@
+'use client';
import type React from 'react';
import { useRef } from 'react';
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
diff --git a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
index f3b6fb431..37bc364d8 100644
--- a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
+++ b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
@@ -1,3 +1,14 @@
+'use client';
+
+import { createContext, useContext, useRef, useState } from 'react';
+import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
+import {
+ type DisclosureGroupProps,
+ type DisclosureGroupState,
+ useDisclosureGroupState,
+ useDisclosureState,
+} from 'react-stately';
+
interface Props {
groups: TDisclosureGroup[];
icon?: React.ReactNode;
@@ -13,15 +24,6 @@ type TDisclosureGroup = {
}[];
};
-import { createContext, useContext, useRef, useState } from 'react';
-import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
-import {
- type DisclosureGroupProps,
- type DisclosureGroupState,
- useDisclosureGroupState,
- useDisclosureState,
-} from 'react-stately';
-
const DisclosureGroupStateContext = createContext(null);
/**
diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx
index cd2290198..d6bb785e7 100644
--- a/packages/react-openapi/src/OpenAPIResponseExample.tsx
+++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx
@@ -1,6 +1,6 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
-import { InteractiveSection } from './InteractiveSection';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
+import { StaticSection } from './StaticSection';
import { generateSchemaExample } from './generateSchemaExample';
import { json2xml } from './json2xml';
import { stringifyOpenAPI } from './stringifyOpenAPI';
@@ -84,9 +84,9 @@ export function OpenAPIResponseExample(props: {
return (
- } className="openapi-response-example">
+ } className="openapi-response-example">
-
+
);
}
@@ -134,12 +134,9 @@ function OpenAPIResponse(props: {
return (
- }
- className="openapi-response-media-types"
- >
+ } className="openapi-response-media-types">
-
+
);
}
@@ -173,23 +170,19 @@ function OpenAPIResponseMediaType(props: {
key: example.key,
label: example.example.summary || example.key,
body: (
-
+
),
};
});
return (
- }
className="openapi-response-media-type-examples"
>
-
+
);
}
diff --git a/packages/react-openapi/src/OpenAPIResponses.tsx b/packages/react-openapi/src/OpenAPIResponses.tsx
index 89a59c00d..c434073c6 100644
--- a/packages/react-openapi/src/OpenAPIResponses.tsx
+++ b/packages/react-openapi/src/OpenAPIResponses.tsx
@@ -1,8 +1,8 @@
import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
-import { InteractiveSection } from './InteractiveSection';
import { Markdown } from './Markdown';
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
import { OpenAPIResponse } from './OpenAPIResponse';
+import { StaticSection } from './StaticSection';
import type { OpenAPIClientContext } from './types';
/**
@@ -15,7 +15,7 @@ export function OpenAPIResponses(props: {
const { responses, context } = props;
return (
-
+
-
+
);
}
diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx
index a98aa373c..93a628d26 100644
--- a/packages/react-openapi/src/OpenAPISchema.tsx
+++ b/packages/react-openapi/src/OpenAPISchema.tsx
@@ -119,9 +119,9 @@ export function OpenAPISchemaProperties(props: {
return (
- {properties.map((property) => (
+ {properties.map((property, index) => (
{
return (
-
-
+
);
})}
diff --git a/packages/react-openapi/src/OpenAPITabs.tsx b/packages/react-openapi/src/OpenAPITabs.tsx
index 698fb58b5..6640cd49d 100644
--- a/packages/react-openapi/src/OpenAPITabs.tsx
+++ b/packages/react-openapi/src/OpenAPITabs.tsx
@@ -1,10 +1,9 @@
'use client';
-import { createContext, useContext, useEffect, useMemo, useState } from 'react';
+import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
-import { useIntersectionObserver } from 'usehooks-ts';
import { Markdown } from './Markdown';
-import { useSyncedTabsGlobalState } from './useSyncedTabsGlobalState';
+import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
export type TabItem = {
key: Key;
@@ -15,7 +14,7 @@ export type TabItem = {
type OpenAPITabsContextData = {
items: TabItem[];
- selectedTab: TabItem;
+ selectedTab: TabItem | null;
};
const OpenAPITabsContext = createContext(null);
@@ -35,68 +34,44 @@ export function OpenAPITabs(
props: React.PropsWithChildren
) {
const { children, items, stateKey } = props;
- const [ref, isIntersectionVisible] = useIntersectionObserver({
- threshold: 0.1,
- rootMargin: '200px',
- });
- const isVisible = stateKey ? isIntersectionVisible : true;
- const defaultTab = items[0] as TabItem;
- const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState();
- const [selectedTabKey, setSelectedTabKey] = useState(() => {
- if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
- const tabFromState = syncedTabs.get(stateKey);
- return tabFromState?.key ?? items[0]?.key;
- }
- return items[0]?.key;
- });
- const [selectedTab, setSelectedTab] = useState(defaultTab);
-
- const handleSelectionChange = (key: Key) => {
- setSelectedTabKey(key);
- if (stateKey) {
- const tab = items.find((item) => item.key === key);
-
- if (!tab) {
- return;
+ const [tabKey, setTabKey] = useState(() => {
+ if (stateKey && typeof window !== 'undefined') {
+ const store = getOrCreateTabStoreByKey(stateKey);
+ const tabKey = store.getState().tabKey;
+ if (tabKey) {
+ return tabKey;
}
-
- setSyncedTabs((state) => {
- const newState = new Map(state);
- newState.set(stateKey, tab);
- return newState;
- });
}
- };
-
+ return items[0]?.key ?? null;
+ });
+ const selectedTab = items.find((item) => item.key === tabKey) ?? items[0] ?? null;
+ const cancelDeferRef = useRef<(() => void) | null>(null);
useEffect(() => {
- if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
- const tabFromState = syncedTabs.get(stateKey);
-
- if (!items.some((item) => item.key === tabFromState?.key)) {
- return setSelectedTab(defaultTab);
- }
-
- if (tabFromState && tabFromState?.key !== selectedTab?.key) {
- const tabFromItems = items.find((item) => item.key === tabFromState.key);
-
- if (!tabFromItems) {
- return;
- }
-
- setSelectedTab(tabFromItems);
- }
+ if (!stateKey) {
+ return undefined;
}
- }, [isVisible, stateKey, syncedTabs, selectedTabKey]);
-
+ const store = getOrCreateTabStoreByKey(stateKey);
+ return store.subscribe((state) => {
+ cancelDeferRef.current?.();
+ cancelDeferRef.current = defer(() => setTabKey(state.tabKey));
+ });
+ }, [stateKey]);
+ useEffect(() => {
+ return () => cancelDeferRef.current?.();
+ }, []);
const contextValue = useMemo(() => ({ items, selectedTab }), [items, selectedTab]);
-
return (
{
+ setTabKey(tabKey);
+ if (stateKey) {
+ const store = getOrCreateTabStoreByKey(stateKey);
+ store.setState({ tabKey });
+ }
+ }}
+ selectedKey={tabKey}
>
{children}
@@ -104,6 +79,11 @@ export function OpenAPITabs(
);
}
+const defer = (fn: () => void) => {
+ const id = setTimeout(fn, 0);
+ return () => clearTimeout(id);
+};
+
/**
* The OpenAPI Tabs list component.
* This component should be used as a child of the OpenAPITabs component.
@@ -116,14 +96,14 @@ export function OpenAPITabsList() {
{items.map((tab) => (
({
outline: isFocusVisible
? '2px solid rgb(var(--primary-color-500)/0.4)'
: 'none',
})}
className="openapi-tabs-tab"
- key={`Tab-${tab.key}`}
- id={tab.key}
>
{tab.label}
@@ -144,12 +124,10 @@ export function OpenAPITabsPanels() {
return null;
}
+ const key = selectedTab.key.toString();
+
return (
-
+
{selectedTab.body}
{selectedTab.description ? (
diff --git a/packages/react-openapi/src/StaticSection.tsx b/packages/react-openapi/src/StaticSection.tsx
new file mode 100644
index 000000000..5124227e7
--- /dev/null
+++ b/packages/react-openapi/src/StaticSection.tsx
@@ -0,0 +1,59 @@
+import clsx from 'clsx';
+import { type ComponentPropsWithoutRef, forwardRef } from 'react';
+
+export function Section(props: ComponentPropsWithoutRef<'div'>) {
+ return ;
+}
+
+export function SectionHeader(props: ComponentPropsWithoutRef<'div'>) {
+ return (
+
+ );
+}
+
+export function SectionHeaderContent(props: ComponentPropsWithoutRef<'div'>) {
+ return (
+
+ );
+}
+
+export const SectionBody = forwardRef(function SectionBody(
+ props: ComponentPropsWithoutRef<'div'>,
+ ref: React.ForwardedRef
+) {
+ return (
+
+ );
+});
+
+export function StaticSection(props: {
+ className: string;
+ header: React.ReactNode;
+ children: React.ReactNode;
+}) {
+ const { className, header, children } = props;
+ return (
+
+
+ {header}
+
+ {children}
+
+ );
+}
diff --git a/packages/react-openapi/src/useSyncedTabsGlobalState.ts b/packages/react-openapi/src/useSyncedTabsGlobalState.ts
index fff4674bb..42a6c3a09 100644
--- a/packages/react-openapi/src/useSyncedTabsGlobalState.ts
+++ b/packages/react-openapi/src/useSyncedTabsGlobalState.ts
@@ -1,23 +1,35 @@
'use client';
-import { create } from 'zustand';
+import { createStore } from 'zustand';
-interface SyncedTabsState {
- tabs: Map;
- setTabs: (updater: (tabs: Map) => Map) => void;
-}
+type Key = string | number;
-const useSyncedTabsStore = create>()((set) => ({
- tabs: new Map(),
- setTabs: (updater) =>
- set((state) => ({
- tabs: updater(new Map(state.tabs)), // Ensure a new Map is created for reactivity
- })),
-}));
+type TabState = {
+ tabKey: Key | null;
+};
-// Selector for better performance - only re-renders when tabs change
-export function useSyncedTabsGlobalState() {
- const tabs = useSyncedTabsStore((state) => state.tabs as Map);
- const setTabs = useSyncedTabsStore((state) => state.setTabs as SyncedTabsState['setTabs']);
- return [tabs, setTabs] as const;
-}
+type TabActions = { setTabKey: (tab: Key | null) => void };
+
+type TabStore = TabState & TabActions;
+
+const createTabStore = (initialTab?: Key) => {
+ return createStore()((set) => ({
+ tabKey: initialTab ?? null,
+ setTabKey: (tabKey) => {
+ set(() => ({ tabKey }));
+ },
+ }));
+};
+
+const defaultTabStores = new Map>();
+
+const createTabStoreFactory = (stores: typeof defaultTabStores) => {
+ return (storeKey: string, initialKey?: Key) => {
+ if (!stores.has(storeKey)) {
+ stores.set(storeKey, createTabStore(initialKey));
+ }
+ return stores.get(storeKey)!;
+ };
+};
+
+export const getOrCreateTabStoreByKey = createTabStoreFactory(defaultTabStores);