Skip to content

Commit 583f646

Browse files
authored
Fix nested anyOf/oneOf schema rendering with lazy tabs (#1248)
* Fix nested anyOf/oneOf schema rendering with lazy tabs and proper spacing - Add lazy rendering to SchemaTabs to ensure only selected tab content is rendered - Generate unique IDs for each anyOf/oneOf tab group to prevent value collisions - Add fallback labels for schemas without title or type - Support implicit object types (schemas with properties but no explicit type: object) - Add proper spacing (1rem) between badges and tabs, and after tab containers Fixes rendering issues where: - All tab contents were visible simultaneously regardless of selection - Nested oneOf schemas weren't displaying correctly - Tab selection wasn't properly isolated - Visual spacing was missing between badges and tabs * Add test case for nested anyOf/oneOf with properties at same level Adds /anyof-nested-oneof-with-properties test operation that demonstrates: - anyOf containing multiple oneOf arrays - oneOf schemas with properties but no explicit type: object - Schema with both oneOf and properties at the same level (layer3) - Complex nesting similar to ethernet-interfaces schema This test case validates the fixes for lazy tab rendering, unique tab IDs, and proper handling of implicit object types.
1 parent b0d2b7d commit 583f646

File tree

3 files changed

+219
-9
lines changed

3 files changed

+219
-9
lines changed

demo/examples/tests/anyOf.yaml

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,185 @@ paths:
123123
example: pencil
124124
required:
125125
- orderNo
126+
127+
/anyof-nested-oneof-with-properties:
128+
post:
129+
tags:
130+
- anyOf
131+
summary: anyOf with nested oneOf and properties at same level
132+
description: |
133+
Schema demonstrating complex nested structures where:
134+
- An anyOf contains multiple oneOf arrays
135+
- oneOf schemas have properties without explicit type
136+
- A schema has both oneOf and properties at the same level (layer3)
137+
138+
This pattern is similar to the ethernet-interfaces schema.
139+
140+
Schema:
141+
```yaml
142+
type: object
143+
properties:
144+
id:
145+
type: string
146+
name:
147+
type: string
148+
anyOf:
149+
- oneOf:
150+
- title: tap
151+
properties:
152+
tap:
153+
type: object
154+
default: {}
155+
- title: layer2
156+
required:
157+
- layer2
158+
properties:
159+
layer2:
160+
type: object
161+
properties:
162+
vlan-tag:
163+
type: integer
164+
- title: layer3
165+
required:
166+
- layer3
167+
properties:
168+
layer3:
169+
type: object
170+
oneOf:
171+
- title: static
172+
type: object
173+
properties:
174+
ip:
175+
type: array
176+
items:
177+
type: string
178+
- title: dhcp
179+
type: object
180+
properties:
181+
dhcp-enabled:
182+
type: boolean
183+
properties:
184+
mtu:
185+
type: integer
186+
default: 1500
187+
management-profile:
188+
type: string
189+
- oneOf:
190+
- title: folder
191+
type: object
192+
properties:
193+
folder:
194+
type: string
195+
required:
196+
- folder
197+
- title: snippet
198+
type: object
199+
properties:
200+
snippet:
201+
type: string
202+
required:
203+
- snippet
204+
```
205+
requestBody:
206+
required: true
207+
content:
208+
application/json:
209+
schema:
210+
type: object
211+
properties:
212+
id:
213+
type: string
214+
description: UUID of the resource
215+
example: 123e4567-e89b-12d3-a456-426655440000
216+
name:
217+
type: string
218+
description: Interface name
219+
example: ethernet1/1
220+
anyOf:
221+
- oneOf:
222+
- title: tap
223+
properties:
224+
tap:
225+
type: object
226+
default: {}
227+
- title: layer2
228+
required:
229+
- layer2
230+
properties:
231+
layer2:
232+
type: object
233+
properties:
234+
vlan-tag:
235+
description: Assign interface to VLAN tag
236+
type: integer
237+
minimum: 1
238+
maximum: 4094
239+
- title: layer3
240+
required:
241+
- layer3
242+
properties:
243+
layer3:
244+
type: object
245+
oneOf:
246+
- title: static
247+
type: object
248+
properties:
249+
ip:
250+
description: Interface IP addresses
251+
type: array
252+
items:
253+
type: string
254+
example: 192.168.1.1/24
255+
- title: dhcp
256+
type: object
257+
properties:
258+
dhcp-enabled:
259+
description: Enable DHCP client
260+
type: boolean
261+
default: true
262+
properties:
263+
mtu:
264+
description: Maximum transmission unit
265+
type: integer
266+
minimum: 576
267+
maximum: 9216
268+
default: 1500
269+
management-profile:
270+
description: Interface management profile
271+
type: string
272+
maxLength: 31
273+
- oneOf:
274+
- type: object
275+
title: folder
276+
properties:
277+
folder:
278+
type: string
279+
pattern: ^[a-zA-Z\d-_\. ]+$
280+
maxLength: 64
281+
description: The folder in which the resource is defined
282+
example: My Folder
283+
required:
284+
- folder
285+
- type: object
286+
title: snippet
287+
properties:
288+
snippet:
289+
type: string
290+
pattern: ^[a-zA-Z\d-_\. ]+$
291+
maxLength: 64
292+
description: The snippet in which the resource is defined
293+
example: My Snippet
294+
required:
295+
- snippet
296+
responses:
297+
"201":
298+
description: Created
299+
content:
300+
application/json:
301+
schema:
302+
type: object
303+
properties:
304+
id:
305+
type: string
306+
name:
307+
type: string

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

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88
import React from "react";
99

1010
import { translate } from "@docusaurus/Translate";
11-
import { OPENAPI_SCHEMA_ITEM } from "@theme/translationIds";
12-
1311
import { ClosingArrayBracket, OpeningArrayBracket } from "@theme/ArrayBrackets";
1412
import Details from "@theme/Details";
1513
import DiscriminatorTabs from "@theme/DiscriminatorTabs";
1614
import Markdown from "@theme/Markdown";
1715
import SchemaItem from "@theme/SchemaItem";
1816
import SchemaTabs from "@theme/SchemaTabs";
1917
import TabItem from "@theme/TabItem";
18+
import { OPENAPI_SCHEMA_ITEM } from "@theme/translationIds";
2019
// eslint-disable-next-line import/no-extraneous-dependencies
2120
import { merge } from "allof-merge";
2221
import clsx from "clsx";
@@ -132,20 +131,44 @@ const AnyOneOf: React.FC<SchemaProps> = ({ schema, schemaType }) => {
132131
const type = schema.oneOf
133132
? translate({ id: OPENAPI_SCHEMA_ITEM.ONE_OF, message: "oneOf" })
134133
: translate({ id: OPENAPI_SCHEMA_ITEM.ANY_OF, message: "anyOf" });
134+
135+
// Generate a unique ID for this anyOf/oneOf to prevent tab value collisions
136+
const uniqueId = React.useMemo(
137+
() => Math.random().toString(36).substring(7),
138+
[]
139+
);
140+
135141
return (
136142
<>
137143
<span className="badge badge--info" style={{ marginBottom: "1rem" }}>
138144
{type}
139145
</span>
140-
<SchemaTabs>
146+
<SchemaTabs groupId={`schema-${uniqueId}`} lazy>
141147
{schema[key]?.map((anyOneSchema: any, index: number) => {
142-
const label = anyOneSchema.title || anyOneSchema.type;
148+
// Determine label for the tab
149+
// If schema is just oneOf/anyOf without title/type, use a generic label
150+
let label = anyOneSchema.title || anyOneSchema.type;
151+
if (!label) {
152+
if (anyOneSchema.oneOf) {
153+
label = translate({
154+
id: OPENAPI_SCHEMA_ITEM.ONE_OF,
155+
message: "oneOf",
156+
});
157+
} else if (anyOneSchema.anyOf) {
158+
label = translate({
159+
id: OPENAPI_SCHEMA_ITEM.ANY_OF,
160+
message: "anyOf",
161+
});
162+
} else {
163+
label = `Option ${index + 1}`;
164+
}
165+
}
143166
return (
144167
// @ts-ignore
145168
<TabItem
146169
key={index}
147170
label={label}
148-
value={`${index}-item-properties`}
171+
value={`${uniqueId}-${index}-item`}
149172
>
150173
{/* Handle primitive types directly */}
151174
{(isPrimitive(anyOneSchema) || anyOneSchema.const) && (
@@ -178,9 +201,11 @@ const AnyOneOf: React.FC<SchemaProps> = ({ schema, schemaType }) => {
178201
)}
179202

180203
{/* Handle actual object types with properties or nested schemas */}
181-
{anyOneSchema.type === "object" && anyOneSchema.properties && (
182-
<Properties schema={anyOneSchema} schemaType={schemaType} />
183-
)}
204+
{/* Note: In OpenAPI, properties implies type: object even if not explicitly set */}
205+
{(anyOneSchema.type === "object" || !anyOneSchema.type) &&
206+
anyOneSchema.properties && (
207+
<Properties schema={anyOneSchema} schemaType={schemaType} />
208+
)}
184209
{anyOneSchema.allOf && (
185210
<SchemaNode schema={anyOneSchema} schemaType={schemaType} />
186211
)}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,10 @@ function TabList({
115115
};
116116

117117
return (
118-
<div className="openapi-tabs__schema-tabs-container">
118+
<div
119+
className="openapi-tabs__schema-tabs-container"
120+
style={{ marginBottom: "1rem" }}
121+
>
119122
{showTabArrows && (
120123
<button
121124
className="openapi-tabs__arrow left"

0 commit comments

Comments
 (0)