Sync responses selector with the Responses collapsibles (#4376)

This commit is contained in:
Nolann B.
2026-07-08 09:08:21 +02:00
committed by GitHub
parent e14609cb3e
commit 597fe34d3b
5 changed files with 58 additions and 48 deletions
@@ -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.
@@ -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);
},
});
@@ -93,13 +93,7 @@ export function OpenAPIResponseExample(props: {
return null;
}
return (
<OpenAPIResponseExampleContent
selectIcon={context.icons.chevronDown}
blockKey={context.blockKey}
items={tabs}
/>
);
return <OpenAPIResponseExampleContent selectIcon={context.icons.chevronDown} items={tabs} />;
}
function OpenAPIResponse(props: {
@@ -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 (
<StaticSection
header={
<OpenAPIResponseExampleHeader
selectIcon={selectIcon}
blockKey={blockKey}
items={items}
/>
}
header={<OpenAPIResponseExampleHeader selectIcon={selectIcon} items={items} />}
className="openapi-response-examples"
>
<OpenAPIResponseExampleBody blockKey={blockKey} items={items} />
<OpenAPIResponseExampleBody items={items} />
</StaticSection>
);
}
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: {
<OpenAPISelect
items={items}
icon={selectIcon}
stateKey={getResponseExampleStateKey(blockKey)}
stateKey={getResponseExampleStateKey()}
placement="bottom start"
>
{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');
}
+30 -11
View File
@@ -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<string>();
// 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<Key>) => {
state.setKey(keys.values().next().value ?? null);
},
};
return (
<StaticSection header={t(context.translation, 'responses')} className="openapi-responses">
<OpenAPIDisclosureGroup
icon={context.icons.chevronRight}
allowsMultipleExpanded={expandAll}
expandedKeys={expandedKeys}
onExpandedChange={(keys) => {
const key = keys.values().next().value ?? null;
state.setKey(key);
}}
groups={groups}
selectIcon={context.icons.chevronDown}
selectStateKey={createStateKey('response-media-types', context.blockKey)}
{...disclosureProps}
/>
</StaticSection>
);
}
/**
* 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<string> {
if (selectedKey == null) {
return new Set<string>();
}
const key = groups.find((g) => g.key === selectedKey)?.key ?? groups[0]?.key;
return key ? new Set([key]) : new Set<string>();
}