Skip to content

Commit 0cdec2e

Browse files
sserrataclaude
andauthored
feat(theme): configurable schema expansion level (#1222) (#1449)
* feat(theme): configurable schema expansion level (#1222) Adds an opt-in `themeConfig.api.schemaExpansion` option that controls the default expansion depth of nested request/response schema trees, plus an inline icon control next to each schema header that lets readers change the depth at view time. The selected depth is persisted in localStorage. - New `SchemaExpansionProvider` and `SchemaDepthProvider` contexts thread the active expansion level and per-node depth without prop-drilling through the recursive Schema renderer. - `SchemaNodeDetails` opens its `<Details>` when `depth < level` and remounts on level change so the control feels responsive. - New `SchemaExpansionControl` renders an icon trigger inline with the Body/Schema headers; clicking opens a compact popover with depth options including "All". - Behavior is unchanged when `schemaExpansion` is unset. Demo: enables the option and adds an envelope-wrapped fixture under `demo/examples/tests/schemaExpansion.yaml` for manual exercise. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style(theme): remove pointer cursor on schema expansion control Defaults to the standard arrow cursor on hover for a less clicky feel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style(theme): fix wide click target on schema expansion trigger Lock the trigger button to a 22x22 box so the hover background and click area match the icon, rather than stretching to the flex row's height. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(theme): render schema expansion popover in a portal When the surrounding <details> was collapsed, the absolutely-positioned popover got clipped by the details element. Render the popover via a portal to document.body with fixed positioning computed from the trigger's bounding rect, and reposition on scroll/resize. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(theme): use fixed positioning instead of portal for popover Avoid pulling in @types/react-dom by computing the trigger's bounding rect and rendering the popover with position: fixed in-place. Fixed positioning still escapes the surrounding details overflow, so the menu is no longer clipped when the schema is collapsed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style(theme): keep REQUIRED label inline with schema title When the parent summary became a flex row, the h3/strong holding the title and the REQUIRED span fell back to block layout and wrapped. Make the title element itself an inline-flex with a small gap so the title and the badge stay on one line and align vertically. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(theme): clarify tooltip on schema expansion control Switch the aria-label / title from "Schema expansion depth" to a more descriptive "Set how deep schemas auto-expand" so hovering the icon explains what the control does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(theme): close schema expansion popover on scroll Repositioning the popover during scroll lagged behind the trigger and felt detached. Close the popover instead so it always appears anchored at the moment of opening. Also revert the tooltip text to its original "Schema expansion depth". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(theme): polish a11y, i18n, and BEM for schema expansion control - Centralize translation IDs under OPENAPI_SCHEMA_EXPANSION in translationIds.ts so they can be discovered alongside other theme strings and overridden via i18n catalogs. - aria-haspopup="menu" (was "true") + aria-controls + popover id + aria-label on the menu so assistive tech announces it as a menu. - Move focus to the active option when the menu opens; arrow-key navigation between options; ESC stops propagation so it doesn't also collapse a parent details element. - Per-option aria-label "Expand to depth N" via translate() so screen readers get a full description instead of just a number. - Class names already follow BEM (block, __element, --modifier). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(theme): apply default expansion depth even when control is hidden `themeConfig.api.schemaExpansion.default` now reliably auto-expands to the configured depth on every page load when `enabled` is false — the provider previously kept reading from localStorage in that case, so a stale value from a prior session where the control was enabled could shadow the new default. Persistence is now implicitly off whenever the control is hidden. Also clarify the docstrings so it's discoverable that you can set just `{ default: 1 }` to get auto-expansion without rendering the UI. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7115da3 commit 0cdec2e

12 files changed

Lines changed: 772 additions & 79 deletions

File tree

demo/docusaurus.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,13 @@ const config: Config = {
271271
id: "announcementBar_2",
272272
content: "v5.0.0 is now available! Requires Docusaurus 3.10.0+",
273273
},
274+
api: {
275+
schemaExpansion: {
276+
enabled: true,
277+
default: 1,
278+
max: 4,
279+
},
280+
},
274281
} satisfies Preset.ThemeConfig,
275282

276283
plugins: [
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Schema Expansion Demo API
4+
version: 1.0.0
5+
description: |
6+
Exercises the configurable schema expansion control. Endpoints below return
7+
deeply nested envelope-wrapped payloads where the useful fields live a few
8+
levels under `data`. The expansion pill control on each page should let
9+
readers reveal those fields without manual clicking.
10+
11+
Tracks: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1222
12+
13+
tags:
14+
- name: expansion
15+
description: Default schema expansion level demos
16+
17+
paths:
18+
/users/{id}:
19+
get:
20+
tags:
21+
- expansion
22+
summary: Get user (envelope-wrapped)
23+
description: |
24+
Wrapped in a typical `{ status, meta, data }` envelope where `data`
25+
itself contains a nested `profile` object. With expansion level `1`,
26+
`data` opens automatically; with level `2`, `profile` opens too.
27+
parameters:
28+
- name: id
29+
in: path
30+
required: true
31+
schema:
32+
type: string
33+
responses:
34+
"200":
35+
description: A wrapped user payload.
36+
content:
37+
application/json:
38+
schema:
39+
$ref: "#/components/schemas/UserEnvelope"
40+
/users:
41+
post:
42+
tags:
43+
- expansion
44+
summary: Create user (nested request body)
45+
description: |
46+
The request body is also envelope-wrapped, exercising the expansion
47+
control on the request side.
48+
requestBody:
49+
required: true
50+
content:
51+
application/json:
52+
schema:
53+
$ref: "#/components/schemas/CreateUserEnvelope"
54+
responses:
55+
"201":
56+
description: Created.
57+
content:
58+
application/json:
59+
schema:
60+
$ref: "#/components/schemas/UserEnvelope"
61+
62+
components:
63+
schemas:
64+
Status:
65+
type: object
66+
properties:
67+
code:
68+
type: integer
69+
message:
70+
type: string
71+
72+
Meta:
73+
type: object
74+
properties:
75+
requestId:
76+
type: string
77+
format: uuid
78+
timestamp:
79+
type: string
80+
format: date-time
81+
82+
Address:
83+
type: object
84+
properties:
85+
street:
86+
type: string
87+
city:
88+
type: string
89+
country:
90+
type: string
91+
92+
Profile:
93+
type: object
94+
properties:
95+
displayName:
96+
type: string
97+
bio:
98+
type: string
99+
address:
100+
$ref: "#/components/schemas/Address"
101+
102+
User:
103+
type: object
104+
properties:
105+
id:
106+
type: string
107+
email:
108+
type: string
109+
format: email
110+
profile:
111+
$ref: "#/components/schemas/Profile"
112+
113+
UserEnvelope:
114+
type: object
115+
properties:
116+
status:
117+
$ref: "#/components/schemas/Status"
118+
meta:
119+
$ref: "#/components/schemas/Meta"
120+
data:
121+
$ref: "#/components/schemas/User"
122+
123+
CreateUserInput:
124+
type: object
125+
required:
126+
- email
127+
properties:
128+
email:
129+
type: string
130+
format: email
131+
profile:
132+
$ref: "#/components/schemas/Profile"
133+
134+
CreateUserEnvelope:
135+
type: object
136+
properties:
137+
meta:
138+
$ref: "#/components/schemas/Meta"
139+
data:
140+
$ref: "#/components/schemas/CreateUserInput"

packages/docusaurus-theme-openapi-docs/src/theme/ApiItem/index.tsx

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import DocItemLayout from "@theme/ApiItem/Layout";
2020
import CodeBlock from "@theme/CodeBlock";
2121
import type { Props } from "@theme/DocItem";
2222
import DocItemMetadata from "@theme/DocItem/Metadata";
23+
import { SchemaExpansionProvider } from "@theme/SchemaExpansion";
2324
import SkeletonLoader from "@theme/SkeletonLoader";
2425
import clsx from "clsx";
2526
import type {
@@ -177,18 +178,20 @@ export default function ApiItem(props: Props): JSX.Element {
177178
<DocItemMetadata />
178179
<DocItemLayout>
179180
<Provider store={store2}>
180-
<div className={clsx("row", "theme-api-markdown")}>
181-
<div className="col col--7 openapi-left-panel__container">
182-
<MDXComponent />
183-
</div>
184-
<div className="col col--5 openapi-right-panel__container">
185-
<BrowserOnly fallback={<SkeletonLoader size="lg" />}>
186-
{() => {
187-
return <ApiExplorer item={api} infoPath={infoPath} />;
188-
}}
189-
</BrowserOnly>
181+
<SchemaExpansionProvider>
182+
<div className={clsx("row", "theme-api-markdown")}>
183+
<div className="col col--7 openapi-left-panel__container">
184+
<MDXComponent />
185+
</div>
186+
<div className="col col--5 openapi-right-panel__container">
187+
<BrowserOnly fallback={<SkeletonLoader size="lg" />}>
188+
{() => {
189+
return <ApiExplorer item={api} infoPath={infoPath} />;
190+
}}
191+
</BrowserOnly>
192+
</div>
190193
</div>
191-
</div>
194+
</SchemaExpansionProvider>
192195
</Provider>
193196
</DocItemLayout>
194197
</HtmlClassNameProvider>
@@ -200,16 +203,18 @@ export default function ApiItem(props: Props): JSX.Element {
200203
<HtmlClassNameProvider className={docHtmlClassName}>
201204
<DocItemMetadata />
202205
<DocItemLayout>
203-
<div className={clsx("row", "theme-api-markdown")}>
204-
<div className="col col--7 openapi-left-panel__container schema">
205-
<MDXComponent />
206-
</div>
207-
<div className="col col--5 openapi-right-panel__container">
208-
<CodeBlock language="json" title={`${frontMatter.title}`}>
209-
{JSON.stringify(sample, null, 2)}
210-
</CodeBlock>
206+
<SchemaExpansionProvider>
207+
<div className={clsx("row", "theme-api-markdown")}>
208+
<div className="col col--7 openapi-left-panel__container schema">
209+
<MDXComponent />
210+
</div>
211+
<div className="col col--5 openapi-right-panel__container">
212+
<CodeBlock language="json" title={`${frontMatter.title}`}>
213+
{JSON.stringify(sample, null, 2)}
214+
</CodeBlock>
215+
</div>
211216
</div>
212-
</div>
217+
</SchemaExpansionProvider>
213218
</DocItemLayout>
214219
</HtmlClassNameProvider>
215220
</DocProvider>

packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
ResponseExamples,
1919
} from "@theme/ResponseExamples";
2020
import SchemaNode from "@theme/Schema";
21+
import SchemaExpansionControl from "@theme/SchemaExpansion";
2122
import SchemaTabs from "@theme/SchemaTabs";
2223
import SkeletonLoader from "@theme/SkeletonLoader";
2324
import TabItem from "@theme/TabItem";
@@ -77,24 +78,23 @@ const RequestSchemaComponent: React.FC<Props> = ({ title, body, style }) => {
7778
open={true}
7879
style={style}
7980
summary={
80-
<>
81-
<summary>
82-
<h3 className="openapi-markdown__details-summary-header-body">
83-
{translate({
84-
id: OPENAPI_REQUEST.BODY_TITLE,
85-
message: title,
86-
})}
87-
{body.required === true && (
88-
<span className="openapi-schema__required">
89-
{translate({
90-
id: OPENAPI_SCHEMA_ITEM.REQUIRED,
91-
message: "required",
92-
})}
93-
</span>
94-
)}
95-
</h3>
96-
</summary>
97-
</>
81+
<summary className="openapi-markdown__details-summary--with-control">
82+
<h3 className="openapi-markdown__details-summary-header-body">
83+
{translate({
84+
id: OPENAPI_REQUEST.BODY_TITLE,
85+
message: title,
86+
})}
87+
{body.required === true && (
88+
<span className="openapi-schema__required">
89+
{translate({
90+
id: OPENAPI_SCHEMA_ITEM.REQUIRED,
91+
message: "required",
92+
})}
93+
</span>
94+
)}
95+
</h3>
96+
<SchemaExpansionControl />
97+
</summary>
9898
}
9999
>
100100
<div style={{ textAlign: "left", marginLeft: "1rem" }}>
@@ -164,27 +164,26 @@ const RequestSchemaComponent: React.FC<Props> = ({ title, body, style }) => {
164164
open={true}
165165
style={style}
166166
summary={
167-
<>
168-
<summary>
169-
<h3 className="openapi-markdown__details-summary-header-body">
170-
{translate({
171-
id: OPENAPI_REQUEST.BODY_TITLE,
172-
message: title,
173-
})}
174-
{firstBody.type === "array" && (
175-
<span style={{ opacity: "0.6" }}> array</span>
176-
)}
177-
{body.required && (
178-
<strong className="openapi-schema__required">
179-
{translate({
180-
id: OPENAPI_SCHEMA_ITEM.REQUIRED,
181-
message: "required",
182-
})}
183-
</strong>
184-
)}
185-
</h3>
186-
</summary>
187-
</>
167+
<summary className="openapi-markdown__details-summary--with-control">
168+
<h3 className="openapi-markdown__details-summary-header-body">
169+
{translate({
170+
id: OPENAPI_REQUEST.BODY_TITLE,
171+
message: title,
172+
})}
173+
{firstBody.type === "array" && (
174+
<span style={{ opacity: "0.6" }}> array</span>
175+
)}
176+
{body.required && (
177+
<strong className="openapi-schema__required">
178+
{translate({
179+
id: OPENAPI_SCHEMA_ITEM.REQUIRED,
180+
message: "required",
181+
})}
182+
</strong>
183+
)}
184+
</h3>
185+
<SchemaExpansionControl />
186+
</summary>
188187
}
189188
>
190189
<div style={{ textAlign: "left", marginLeft: "1rem" }}>

packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
ResponseExamples,
1919
} from "@theme/ResponseExamples";
2020
import SchemaNode from "@theme/Schema";
21+
import SchemaExpansionControl from "@theme/SchemaExpansion";
2122
import SchemaTabs from "@theme/SchemaTabs";
2223
import SkeletonLoader from "@theme/SkeletonLoader";
2324
import TabItem from "@theme/TabItem";
@@ -91,21 +92,20 @@ const ResponseSchemaComponent: React.FC<Props> = ({
9192
open={true}
9293
style={style}
9394
summary={
94-
<>
95-
<summary>
96-
<strong className="openapi-markdown__details-summary-response">
97-
{title}
98-
{body.required === true && (
99-
<span className="openapi-schema__required">
100-
{translate({
101-
id: OPENAPI_SCHEMA_ITEM.REQUIRED,
102-
message: "required",
103-
})}
104-
</span>
105-
)}
106-
</strong>
107-
</summary>
108-
</>
95+
<summary className="openapi-markdown__details-summary--with-control">
96+
<strong className="openapi-markdown__details-summary-response">
97+
{title}
98+
{body.required === true && (
99+
<span className="openapi-schema__required">
100+
{translate({
101+
id: OPENAPI_SCHEMA_ITEM.REQUIRED,
102+
message: "required",
103+
})}
104+
</span>
105+
)}
106+
</strong>
107+
<SchemaExpansionControl />
108+
</summary>
109109
}
110110
>
111111
<div style={{ textAlign: "left", marginLeft: "1rem" }}>

0 commit comments

Comments
 (0)