diff --git a/.changeset/rnd-11616-responses-selector-sync.md b/.changeset/rnd-11616-responses-selector-sync.md
new file mode 100644
index 000000000..ab563228f
--- /dev/null
+++ b/.changeset/rnd-11616-responses-selector-sync.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Sync the API reference responses selector with the "Responses" collapsibles, and keep the selected response in sync across every operation on the page (like the code sample language selector). Selecting a status code now expands the matching response section and applies to all operations at once.
diff --git a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
index 71413da3e..c62783fb9 100644
--- a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
+++ b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
@@ -59,10 +59,11 @@ export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
onExpandedChange,
} = props;
- const initialKeys =
- expandedKeys || defaultExpandedKeys
- ? new Set(expandedKeys || defaultExpandedKeys)
- : undefined;
+ // When `expandedKeys` is provided, the group is controlled by the parent. Otherwise it owns
+ // its expanded state in a store keyed by `stateKey`.
+ const isControlled = expandedKeys !== undefined;
+
+ const initialKeys = defaultExpandedKeys ? new Set(defaultExpandedKeys) : undefined;
const { expandedKeys: storeExpandedKeys, setExpandedKeys } = useDisclosureGroupStore(
stateKey,
initialKeys
@@ -70,9 +71,11 @@ export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
const state = useDisclosureGroupState({
...props,
- expandedKeys: storeExpandedKeys,
+ expandedKeys: isControlled ? expandedKeys : storeExpandedKeys,
onExpandedChange: (keys) => {
- setExpandedKeys(keys);
+ if (!isControlled) {
+ setExpandedKeys(keys);
+ }
onExpandedChange?.(keys);
},
});
diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx
index 945ace4f1..794817d06 100644
--- a/packages/react-openapi/src/OpenAPIResponseExample.tsx
+++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx
@@ -93,13 +93,7 @@ export function OpenAPIResponseExample(props: {
return null;
}
- return (
-
- );
+ return ;
}
function OpenAPIResponse(props: {
diff --git a/packages/react-openapi/src/OpenAPIResponseExampleContent.tsx b/packages/react-openapi/src/OpenAPIResponseExampleContent.tsx
index 74c2f1142..58e4a7b92 100644
--- a/packages/react-openapi/src/OpenAPIResponseExampleContent.tsx
+++ b/packages/react-openapi/src/OpenAPIResponseExampleContent.tsx
@@ -14,42 +14,31 @@ type OpenAPIResponseExampleItem = OpenAPISelectItem & {
/**
* Get the state of the response examples select.
*/
-export function useResponseExamplesState(
- blockKey: string | undefined,
- initialKey: Key = 'default'
-) {
- return useSelectState(getResponseExampleStateKey(blockKey), initialKey);
+export function useResponseExamplesState(initialKey: Key = 'default') {
+ return useSelectState(getResponseExampleStateKey(), initialKey);
}
export function OpenAPIResponseExampleContent(props: {
items: OpenAPIResponseExampleItem[];
- blockKey?: string;
selectIcon?: React.ReactNode;
}) {
- const { blockKey, items, selectIcon } = props;
+ const { items, selectIcon } = props;
return (
- }
+ header={}
className="openapi-response-examples"
>
-
+
);
}
function OpenAPIResponseExampleHeader(props: {
items: OpenAPIResponseExampleItem[];
- blockKey?: string;
selectIcon?: React.ReactNode;
}) {
- const { items, blockKey, selectIcon } = props;
+ const { items, selectIcon } = props;
if (items.length === 1) {
const item = items[0];
@@ -69,7 +58,7 @@ function OpenAPIResponseExampleHeader(props: {
{items.map((item) => (
@@ -103,10 +92,9 @@ function OpenAPIResponseExampleItem(props: {
function OpenAPIResponseExampleBody(props: {
items: OpenAPIResponseExampleItem[];
- blockKey?: string;
}) {
- const { blockKey, items } = props;
- const state = useResponseExamplesState(blockKey, items[0]?.key);
+ const { items } = props;
+ const state = useResponseExamplesState(items[0]?.key);
const selectedItem = items.find((item) => item.key === state.key) ?? items[0];
@@ -118,8 +106,9 @@ function OpenAPIResponseExampleBody(props: {
}
/**
- * Return the state key for the response examples.
+ * Return the state key for the response examples. Not scoped to a block so the selected response
+ * stays in sync across every operation on the page (like the code sample language selector).
*/
-function getResponseExampleStateKey(blockKey: string | undefined) {
- return createStateKey('openapi-responses', blockKey);
+function getResponseExampleStateKey() {
+ return createStateKey('openapi-responses');
}
diff --git a/packages/react-openapi/src/OpenAPIResponses.tsx b/packages/react-openapi/src/OpenAPIResponses.tsx
index 0af72cb24..cac99e909 100644
--- a/packages/react-openapi/src/OpenAPIResponses.tsx
+++ b/packages/react-openapi/src/OpenAPIResponses.tsx
@@ -2,6 +2,7 @@
import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
import clsx from 'classnames';
+import type { Key } from 'react-aria';
import { Markdown } from './Markdown';
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
import { OpenAPIResponse } from './OpenAPIResponse';
@@ -105,29 +106,47 @@ export function OpenAPIResponses(props: {
};
});
- const state = useResponseExamplesState(context.blockKey, groups[0]?.key);
+ const state = useResponseExamplesState(groups[0]?.key);
const expandAll = context.expandAllResponses;
- const expandedKeys = expandAll
- ? new Set(groups.map((g) => g.key))
- : state.key
- ? new Set([state.key])
- : new Set();
+
+ // In expand-all mode the group manages its own (multiple) expanded rows; otherwise it's
+ // controlled and stays in sync with the page-wide responses selector.
+ const disclosureProps = expandAll
+ ? {
+ stateKey: createStateKey('openapi-responses-disclosure', context.blockKey),
+ defaultExpandedKeys: groups.map((g) => g.key),
+ }
+ : {
+ expandedKeys: getExpandedResponseKeys(groups, state.key),
+ onExpandedChange: (keys: Set) => {
+ state.setKey(keys.values().next().value ?? null);
+ },
+ };
return (
{
- const key = keys.values().next().value ?? null;
- state.setKey(key);
- }}
groups={groups}
selectIcon={context.icons.chevronDown}
selectStateKey={createStateKey('response-media-types', context.blockKey)}
+ {...disclosureProps}
/>
);
}
+
+/**
+ * Resolve the expanded response for the controlled accordion: nothing when the selection is
+ * cleared, otherwise the selected status code, falling back to the first response when this
+ * operation doesn't define it.
+ */
+function getExpandedResponseKeys(groups: { key: string }[], selectedKey: Key | null): Set {
+ if (selectedKey == null) {
+ return new Set();
+ }
+ const key = groups.find((g) => g.key === selectedKey)?.key ?? groups[0]?.key;
+ return key ? new Set([key]) : new Set();
+}