@gitbook/react-openapi 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/OpenAPICodeSample.jsx +17 -16
  3. package/dist/OpenAPIDisclosure.d.ts +2 -1
  4. package/dist/OpenAPIDisclosure.jsx +1 -1
  5. package/dist/OpenAPIDisclosureGroup.d.ts +1 -1
  6. package/dist/OpenAPIDisclosureGroup.jsx +2 -2
  7. package/dist/OpenAPIOperation.jsx +21 -7
  8. package/dist/OpenAPIPath.d.ts +3 -2
  9. package/dist/OpenAPIPath.jsx +4 -15
  10. package/dist/OpenAPIRequestBody.jsx +1 -1
  11. package/dist/OpenAPIResponse.jsx +1 -1
  12. package/dist/OpenAPIResponseExample.jsx +6 -5
  13. package/dist/OpenAPIResponses.d.ts +1 -1
  14. package/dist/OpenAPIResponses.jsx +2 -2
  15. package/dist/OpenAPISchema.d.ts +5 -1
  16. package/dist/OpenAPISchema.jsx +72 -61
  17. package/dist/OpenAPISchemaName.d.ts +5 -3
  18. package/dist/OpenAPISchemaName.jsx +25 -4
  19. package/dist/OpenAPISecurities.jsx +2 -2
  20. package/dist/OpenAPITabs.d.ts +3 -3
  21. package/dist/OpenAPITabs.jsx +17 -14
  22. package/dist/ScalarApiButton.jsx +1 -1
  23. package/dist/code-samples.js +239 -17
  24. package/dist/contentTypeChecks.d.ts +9 -0
  25. package/dist/contentTypeChecks.js +27 -0
  26. package/dist/generateSchemaExample.js +2 -1
  27. package/dist/resolveOpenAPIOperation.d.ts +3 -3
  28. package/dist/resolveOpenAPIOperation.js +1 -1
  29. package/dist/stringifyOpenAPI.d.ts +1 -1
  30. package/dist/stringifyOpenAPI.js +8 -2
  31. package/dist/tsconfig.build.tsbuildinfo +1 -1
  32. package/dist/types.d.ts +14 -2
  33. package/dist/util/server.d.ts +9 -0
  34. package/dist/util/server.js +44 -0
  35. package/dist/utils.d.ts +2 -2
  36. package/dist/utils.js +7 -6
  37. package/package.json +3 -8
  38. package/src/InteractiveSection.tsx +4 -4
  39. package/src/OpenAPICodeSample.tsx +20 -19
  40. package/src/OpenAPIDisclosure.tsx +4 -3
  41. package/src/OpenAPIDisclosureGroup.tsx +5 -5
  42. package/src/OpenAPIOperation.tsx +32 -10
  43. package/src/OpenAPIOperationContext.tsx +1 -1
  44. package/src/OpenAPIPath.tsx +11 -10
  45. package/src/OpenAPIRequestBody.tsx +2 -2
  46. package/src/OpenAPIResponse.tsx +3 -3
  47. package/src/OpenAPIResponseExample.tsx +7 -6
  48. package/src/OpenAPIResponses.tsx +4 -4
  49. package/src/OpenAPISchema.test.ts +5 -5
  50. package/src/OpenAPISchema.tsx +134 -73
  51. package/src/OpenAPISchemaName.tsx +40 -7
  52. package/src/OpenAPISecurities.tsx +3 -3
  53. package/src/OpenAPITabs.tsx +23 -17
  54. package/src/ScalarApiButton.tsx +3 -3
  55. package/src/code-samples.test.ts +594 -2
  56. package/src/code-samples.ts +238 -17
  57. package/src/contentTypeChecks.ts +35 -0
  58. package/src/generateSchemaExample.ts +22 -18
  59. package/src/json2xml.test.ts +1 -1
  60. package/src/resolveOpenAPIOperation.test.ts +6 -6
  61. package/src/resolveOpenAPIOperation.ts +7 -7
  62. package/src/stringifyOpenAPI.ts +13 -2
  63. package/src/types.ts +11 -1
  64. package/src/util/server.test.ts +58 -0
  65. package/src/util/server.ts +47 -0
  66. package/src/utils.ts +9 -5
  67. package/dist/OpenAPIServerURL.d.ts +0 -11
  68. package/dist/OpenAPIServerURL.jsx +0 -67
  69. package/dist/OpenAPIServerURLVariable.d.ts +0 -8
  70. package/dist/OpenAPIServerURLVariable.jsx +0 -8
  71. package/src/OpenAPIServerURL.tsx +0 -73
  72. package/src/OpenAPIServerURLVariable.tsx +0 -14
@@ -4,11 +4,11 @@ import { useId } from 'react';
4
4
 
5
5
  import { InteractiveSection } from './InteractiveSection';
6
6
  import { Markdown } from './Markdown';
7
+ import { OpenAPIDisclosure } from './OpenAPIDisclosure';
8
+ import { OpenAPISchemaName } from './OpenAPISchemaName';
9
+ import { stringifyOpenAPI } from './stringifyOpenAPI';
7
10
  import type { OpenAPIClientContext } from './types';
8
11
  import { checkIsReference, resolveDescription } from './utils';
9
- import { stringifyOpenAPI } from './stringifyOpenAPI';
10
- import { OpenAPISchemaName } from './OpenAPISchemaName';
11
- import { OpenAPIDisclosure } from './OpenAPIDisclosure';
12
12
 
13
13
  type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
14
14
 
@@ -27,7 +27,7 @@ export function OpenAPISchemaProperty(
27
27
  circularRefs?: CircularRefsIds;
28
28
  context: OpenAPIClientContext;
29
29
  className?: string;
30
- },
30
+ }
31
31
  ) {
32
32
  const {
33
33
  schema,
@@ -47,12 +47,24 @@ export function OpenAPISchemaProperty(
47
47
  ? null
48
48
  : getSchemaAlternatives(schema, new Set(circularRefs.keys()));
49
49
 
50
+ if (alternatives?.[0]?.length) {
51
+ return (
52
+ <OpenAPISchemaAlternativesItem
53
+ {...props}
54
+ circularRefs={circularRefs}
55
+ context={context}
56
+ alternatives={alternatives}
57
+ parentCircularRef={parentCircularRef}
58
+ />
59
+ );
60
+ }
61
+
50
62
  if ((properties && properties.length > 0) || schema.type === 'object') {
51
63
  return (
52
64
  <InteractiveSection id={id} className={clsx('openapi-schema', className)}>
53
65
  <OpenAPISchemaPresentation {...props} />
54
66
  {properties && properties.length > 0 ? (
55
- <OpenAPIDisclosure context={context}>
67
+ <OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
56
68
  <OpenAPISchemaProperties
57
69
  properties={properties}
58
70
  circularRefs={circularRefs}
@@ -60,22 +72,9 @@ export function OpenAPISchemaProperty(
60
72
  />
61
73
  </OpenAPIDisclosure>
62
74
  ) : null}
63
- </InteractiveSection>
64
- );
65
- }
66
-
67
- if (alternatives?.[0]?.length) {
68
- return (
69
- <InteractiveSection id={id} className={clsx('openapi-schema', className)}>
70
- <OpenAPISchemaPresentation {...props} />
71
- {alternatives[0].map((alternative, index) => (
72
- <OpenAPISchemaAlternative
73
- key={`alternative-${index}`}
74
- schema={alternative}
75
- circularRefs={circularRefs}
76
- context={context}
77
- />
78
- ))}
75
+ {parentCircularRef ? (
76
+ <OpenAPISchemaCircularRef id={parentCircularRef} schema={schema} />
77
+ ) : null}
79
78
  </InteractiveSection>
80
79
  );
81
80
  }
@@ -166,16 +165,72 @@ function OpenAPISchemaAlternative(props: {
166
165
  const { schema, circularRefs, context } = props;
167
166
  const id = useId();
168
167
  const subProperties = getSchemaProperties(schema);
168
+ const description = resolveDescription(schema);
169
+ const alternatives = getSchemaAlternatives(schema, new Set(circularRefs?.keys()));
170
+
171
+ if (alternatives?.[0]?.length && !subProperties?.length) {
172
+ return (
173
+ <>
174
+ {description ? (
175
+ <Markdown source={description} className="openapi-schema-description" />
176
+ ) : null}
177
+ <OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
178
+ <OpenAPISchemaAlternativesItem
179
+ schema={schema}
180
+ circularRefs={circularRefs}
181
+ context={context}
182
+ alternatives={alternatives}
183
+ />
184
+ </OpenAPIDisclosure>
185
+ </>
186
+ );
187
+ }
169
188
 
170
189
  return (
171
- <OpenAPIDisclosure context={context}>
172
- <OpenAPISchemaProperties
173
- id={id}
174
- properties={subProperties ?? [{ schema }]}
175
- circularRefs={subProperties ? new Map(circularRefs).set(schema, id) : circularRefs}
176
- context={context}
177
- />
178
- </OpenAPIDisclosure>
190
+ <>
191
+ {description ? (
192
+ <Markdown source={description} className="openapi-schema-description" />
193
+ ) : null}
194
+ <OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
195
+ <OpenAPISchemaProperties
196
+ id={id}
197
+ properties={subProperties ?? [{ schema }]}
198
+ circularRefs={
199
+ subProperties ? new Map(circularRefs).set(schema, id) : circularRefs
200
+ }
201
+ context={context}
202
+ />
203
+ </OpenAPIDisclosure>
204
+ </>
205
+ );
206
+ }
207
+
208
+ function OpenAPISchemaAlternativesItem(
209
+ props: OpenAPISchemaPropertyEntry & {
210
+ circularRefs?: CircularRefsIds;
211
+ context: OpenAPIClientContext;
212
+ alternatives: OpenAPISchemaAlternatives;
213
+ parentCircularRef?: string;
214
+ }
215
+ ) {
216
+ const id = useId();
217
+ const { schema, circularRefs, context, alternatives, parentCircularRef } = props;
218
+
219
+ return (
220
+ <InteractiveSection id={id} className={clsx('openapi-schema')}>
221
+ <OpenAPISchemaPresentation {...props} />
222
+ {alternatives[0].map((alternative, index) => (
223
+ <OpenAPISchemaAlternative
224
+ key={`alternative-${index}`}
225
+ schema={alternative}
226
+ circularRefs={circularRefs}
227
+ context={context}
228
+ />
229
+ ))}
230
+ {parentCircularRef ? (
231
+ <OpenAPISchemaCircularRef id={parentCircularRef} schema={schema} />
232
+ ) : null}
233
+ </InteractiveSection>
179
234
  );
180
235
  }
181
236
 
@@ -219,7 +274,7 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
219
274
 
220
275
  const shouldDisplayExample = (schema: OpenAPIV3.SchemaObject): boolean => {
221
276
  return (
222
- typeof schema.example === 'string' ||
277
+ (typeof schema.example === 'string' && !!schema.example) ||
223
278
  typeof schema.example === 'number' ||
224
279
  typeof schema.example === 'boolean' ||
225
280
  (Array.isArray(schema.example) && schema.example.length > 0) ||
@@ -234,10 +289,10 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
234
289
  return (
235
290
  <div className="openapi-schema-presentation">
236
291
  <OpenAPISchemaName
292
+ schema={schema}
237
293
  type={getSchemaTitle(schema)}
238
294
  propertyName={propertyName}
239
295
  required={required}
240
- deprecated={schema.deprecated}
241
296
  />
242
297
  {schema['x-deprecated-sunset'] ? (
243
298
  <div className="openapi-deprecated-sunset openapi-schema-description openapi-markdown">
@@ -252,12 +307,7 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
252
307
  ) : null}
253
308
  {shouldDisplayExample(schema) ? (
254
309
  <div className="openapi-schema-example">
255
- Example:{' '}
256
- <code>
257
- {typeof schema.example === 'string'
258
- ? schema.example
259
- : stringifyOpenAPI(schema.example)}
260
- </code>
310
+ Example: <code>{formatExample(schema.example)}</code>
261
311
  </div>
262
312
  ) : null}
263
313
  {schema.pattern ? (
@@ -276,17 +326,6 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
276
326
  * Get the sub-properties of a schema.
277
327
  */
278
328
  function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISchemaPropertyEntry[] {
279
- if (schema.allOf) {
280
- return schema.allOf.reduce((acc, subSchema) => {
281
- const properties = getSchemaProperties(subSchema) ?? [
282
- {
283
- schema: subSchema,
284
- },
285
- ];
286
- return [...acc, ...properties];
287
- }, [] as OpenAPISchemaPropertyEntry[]);
288
- }
289
-
290
329
  // check array AND schema.items as this is sometimes null despite what the type indicates
291
330
  if (schema.type === 'array' && !!schema.items) {
292
331
  const items = schema.items;
@@ -295,6 +334,11 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
295
334
  return itemProperties;
296
335
  }
297
336
 
337
+ // If the items are a primitive type, we don't need to display them
338
+ if (['string', 'number', 'boolean', 'integer'].includes(items.type) && !items.enum) {
339
+ return null;
340
+ }
341
+
298
342
  return [
299
343
  {
300
344
  propertyName: 'items',
@@ -333,13 +377,18 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
333
377
  return null;
334
378
  }
335
379
 
380
+ type OpenAPISchemaAlternatives = [
381
+ OpenAPIV3.SchemaObject[],
382
+ OpenAPIV3.DiscriminatorObject | undefined,
383
+ ];
384
+
336
385
  /**
337
386
  * Get the alternatives to display for a schema.
338
387
  */
339
388
  export function getSchemaAlternatives(
340
389
  schema: OpenAPIV3.SchemaObject,
341
- ancestors: Set<OpenAPIV3.SchemaObject> = new Set(),
342
- ): null | [OpenAPIV3.SchemaObject[], OpenAPIV3.DiscriminatorObject | undefined] {
390
+ ancestors: Set<OpenAPIV3.SchemaObject> = new Set()
391
+ ): null | OpenAPISchemaAlternatives {
343
392
  const downAncestors = new Set(ancestors).add(schema);
344
393
 
345
394
  if (schema.anyOf) {
@@ -351,8 +400,7 @@ export function getSchemaAlternatives(
351
400
  }
352
401
 
353
402
  if (schema.allOf) {
354
- // allOf is managed in `getSchemaProperties`
355
- return null;
403
+ return [flattenAlternatives('allOf', schema.allOf, downAncestors), schema.discriminator];
356
404
  }
357
405
 
358
406
  return null;
@@ -361,14 +409,16 @@ export function getSchemaAlternatives(
361
409
  function flattenAlternatives(
362
410
  alternativeType: 'oneOf' | 'allOf' | 'anyOf',
363
411
  alternatives: OpenAPIV3.SchemaObject[],
364
- ancestors: Set<OpenAPIV3.SchemaObject>,
412
+ ancestors: Set<OpenAPIV3.SchemaObject>
365
413
  ): OpenAPIV3.SchemaObject[] {
366
414
  return alternatives.reduce((acc, alternative) => {
367
415
  if (!!alternative[alternativeType] && !ancestors.has(alternative)) {
368
- return [...acc, ...(getSchemaAlternatives(alternative, ancestors)?.[0] || [])];
416
+ acc.push(...(getSchemaAlternatives(alternative, ancestors)?.[0] || []));
417
+ } else {
418
+ acc.push(alternative);
369
419
  }
370
420
 
371
- return [...acc, alternative];
421
+ return acc;
372
422
  }, [] as OpenAPIV3.SchemaObject[]);
373
423
  }
374
424
 
@@ -376,13 +426,8 @@ export function getSchemaTitle(
376
426
  schema: OpenAPIV3.SchemaObject,
377
427
 
378
428
  /** If the title is inferred in a oneOf with discriminator, we can use it to optimize the title */
379
- discriminator?: OpenAPIV3.DiscriminatorObject,
429
+ discriminator?: OpenAPIV3.DiscriminatorObject
380
430
  ): string {
381
- if (schema.title) {
382
- // If the schema has a title, use it
383
- return schema.title;
384
- }
385
-
386
431
  // Try using the discriminator
387
432
  if (discriminator?.propertyName && schema.properties) {
388
433
  const discriminatorProperty = schema.properties[discriminator.propertyName];
@@ -409,7 +454,9 @@ export function getSchemaTitle(
409
454
  if (schema.format) {
410
455
  type += ` · ${schema.format}`;
411
456
  }
412
- } else if ('anyOf' in schema) {
457
+ }
458
+
459
+ if ('anyOf' in schema) {
413
460
  type = 'any of';
414
461
  } else if ('oneOf' in schema) {
415
462
  type = 'one of';
@@ -419,21 +466,35 @@ export function getSchemaTitle(
419
466
  type = 'not';
420
467
  }
421
468
 
422
- if (schema.minimum || schema.minLength) {
423
- type += ` · min: ${schema.minimum || schema.minLength}`;
424
- }
469
+ return type;
470
+ }
425
471
 
426
- if (schema.maximum || schema.maxLength) {
427
- type += ` · max: ${schema.maximum || schema.maxLength}`;
428
- }
472
+ function getDisclosureLabel(schema: OpenAPIV3.SchemaObject): string | undefined {
473
+ if (schema.type === 'array' && !!schema.items) {
474
+ if (schema.items.oneOf) {
475
+ return 'available items';
476
+ }
429
477
 
430
- if (schema.default) {
431
- type += ` · default: ${schema.default}`;
478
+ // Fallback to "child attributes" for enums and objects
479
+ if (schema.items.enum || schema.items.type === 'object') {
480
+ return;
481
+ }
482
+
483
+ return schema.items.title ?? schema.title ?? getSchemaTitle(schema.items);
432
484
  }
433
485
 
434
- if (schema.nullable) {
435
- type = `${type} | nullable`;
486
+ return schema.title;
487
+ }
488
+
489
+ function formatExample(example: any): string {
490
+ if (typeof example === 'string') {
491
+ return example
492
+ .replace(/\n/g, ' ') // Replace newlines with spaces
493
+ .replace(/\s+/g, ' ') // Collapse multiple spaces/newlines into a single space
494
+ .replace(/([\{\}:,])\s+/g, '$1 ') // Ensure a space after {, }, :, and ,
495
+ .replace(/\s+([\{\}:,])/g, ' $1') // Ensure a space before {, }, :, and ,
496
+ .trim();
436
497
  }
437
498
 
438
- return type;
499
+ return stringifyOpenAPI(example);
439
500
  }
@@ -1,27 +1,60 @@
1
+ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2
+ import type React from 'react';
3
+
1
4
  interface OpenAPISchemaNameProps {
2
- propertyName?: string | JSX.Element;
5
+ schema?: OpenAPIV3.SchemaObject;
6
+ propertyName?: string | React.JSX.Element;
3
7
  required?: boolean;
4
8
  type?: string;
5
- deprecated?: boolean;
6
9
  }
7
10
 
8
11
  /**
9
12
  * Display the schema name row.
10
13
  * It includes the property name, type, required and deprecated status.
11
14
  */
12
- export function OpenAPISchemaName(props: OpenAPISchemaNameProps): JSX.Element {
13
- const { type, propertyName, required, deprecated } = props;
15
+ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
16
+ const { schema, type, propertyName, required } = props;
17
+
18
+ const additionalItems = schema && getAdditionalItems(schema);
14
19
 
15
20
  return (
16
21
  <div className="openapi-schema-name">
17
22
  {propertyName ? (
18
- <span data-deprecated={deprecated} className="openapi-schema-propertyname">
23
+ <span data-deprecated={schema?.deprecated} className="openapi-schema-propertyname">
19
24
  {propertyName}
20
25
  </span>
21
26
  ) : null}
22
- {type ? <span className="openapi-schema-type">{type}</span> : null}
27
+ <span>
28
+ {type ? <span className="openapi-schema-type">{type}</span> : null}
29
+ {additionalItems ? (
30
+ <span className="openapi-schema-type">{additionalItems}</span>
31
+ ) : null}
32
+ </span>
23
33
  {required ? <span className="openapi-schema-required">required</span> : null}
24
- {deprecated ? <span className="openapi-deprecated">Deprecated</span> : null}
34
+ {schema?.deprecated ? <span className="openapi-deprecated">Deprecated</span> : null}
25
35
  </div>
26
36
  );
27
37
  }
38
+
39
+ function getAdditionalItems(schema: OpenAPIV3.SchemaObject): string {
40
+ let additionalItems = '';
41
+
42
+ if (schema.minimum || schema.minLength) {
43
+ additionalItems += ` · min: ${schema.minimum || schema.minLength}`;
44
+ }
45
+
46
+ if (schema.maximum || schema.maxLength) {
47
+ additionalItems += ` · max: ${schema.maximum || schema.maxLength}`;
48
+ }
49
+
50
+ // If the schema has a default value, we display it
51
+ if (typeof schema.default !== 'undefined') {
52
+ additionalItems += ` · default: ${schema.default}`;
53
+ }
54
+
55
+ if (schema.nullable) {
56
+ additionalItems = ' | nullable';
57
+ }
58
+
59
+ return additionalItems;
60
+ }
@@ -1,8 +1,8 @@
1
1
  import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
2
- import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
3
2
  import { InteractiveSection } from './InteractiveSection';
4
3
  import { Markdown } from './Markdown';
5
4
  import { OpenAPISchemaName } from './OpenAPISchemaName';
5
+ import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
6
6
  import { resolveDescription } from './utils';
7
7
 
8
8
  /**
@@ -65,7 +65,7 @@ function getLabelForType(security: OpenAPIV3_1.SecuritySchemeObject) {
65
65
  return <OpenAPISchemaName propertyName="Authorization" type="string" required />;
66
66
  }
67
67
 
68
- if (security.scheme == 'bearer') {
68
+ if (security.scheme === 'bearer') {
69
69
  const description = resolveDescription(security);
70
70
  return (
71
71
  <>
@@ -73,7 +73,7 @@ function getLabelForType(security: OpenAPIV3_1.SecuritySchemeObject) {
73
73
  {/** Show a default description if none is provided */}
74
74
  {!description ? (
75
75
  <Markdown
76
- source={`Bearer authentication header of the form Bearer ${`&lt;token&gt;`}.`}
76
+ source={`Bearer authentication header of the form Bearer ${'&lt;token&gt;'}.`}
77
77
  className="openapi-securities-description"
78
78
  />
79
79
  ) : null}
@@ -1,12 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import { createContext, useContext, useEffect, useMemo, useState } from 'react';
4
- import { Key, Tab, TabList, TabPanel, Tabs, TabsProps } from 'react-aria-components';
4
+ import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
5
+ import { useIntersectionObserver } from 'usehooks-ts';
5
6
  import { Markdown } from './Markdown';
6
7
  import { useSyncedTabsGlobalState } from './useSyncedTabsGlobalState';
7
- import { useIntersectionObserver } from 'usehooks-ts';
8
8
 
9
- export type Tab = {
9
+ export type TabItem = {
10
10
  key: Key;
11
11
  label: string;
12
12
  body: React.ReactNode;
@@ -14,8 +14,8 @@ export type Tab = {
14
14
  };
15
15
 
16
16
  type OpenAPITabsContextData = {
17
- items: Tab[];
18
- selectedTab: Tab;
17
+ items: TabItem[];
18
+ selectedTab: TabItem;
19
19
  };
20
20
 
21
21
  const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null);
@@ -32,17 +32,16 @@ function useOpenAPITabsContext() {
32
32
  * The OpenAPI Tabs wrapper component.
33
33
  */
34
34
  export function OpenAPITabs(
35
- props: React.PropsWithChildren<TabsProps & { items: Tab[]; stateKey?: string }>,
35
+ props: React.PropsWithChildren<TabsProps & { items: TabItem[]; stateKey?: string }>
36
36
  ) {
37
37
  const { children, items, stateKey } = props;
38
- const isVisible = stateKey
39
- ? useIntersectionObserver({
40
- threshold: 0.1,
41
- rootMargin: '200px',
42
- })
43
- : true;
44
- const defaultTab = items[0] as Tab;
45
- const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState<Tab>();
38
+ const [ref, isIntersectionVisible] = useIntersectionObserver({
39
+ threshold: 0.1,
40
+ rootMargin: '200px',
41
+ });
42
+ const isVisible = stateKey ? isIntersectionVisible : true;
43
+ const defaultTab = items[0] as TabItem;
44
+ const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState<TabItem>();
46
45
  const [selectedTabKey, setSelectedTabKey] = useState(() => {
47
46
  if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
48
47
  const tabFromState = syncedTabs.get(stateKey);
@@ -50,7 +49,7 @@ export function OpenAPITabs(
50
49
  }
51
50
  return items[0]?.key;
52
51
  });
53
- const [selectedTab, setSelectedTab] = useState<Tab>(defaultTab);
52
+ const [selectedTab, setSelectedTab] = useState<TabItem>(defaultTab);
54
53
 
55
54
  const handleSelectionChange = (key: Key) => {
56
55
  setSelectedTabKey(key);
@@ -74,11 +73,17 @@ export function OpenAPITabs(
74
73
  const tabFromState = syncedTabs.get(stateKey);
75
74
 
76
75
  if (!items.some((item) => item.key === tabFromState?.key)) {
77
- return;
76
+ return setSelectedTab(defaultTab);
78
77
  }
79
78
 
80
79
  if (tabFromState && tabFromState?.key !== selectedTab?.key) {
81
- setSelectedTab(tabFromState);
80
+ const tabFromItems = items.find((item) => item.key === tabFromState.key);
81
+
82
+ if (!tabFromItems) {
83
+ return;
84
+ }
85
+
86
+ setSelectedTab(tabFromItems);
82
87
  }
83
88
  }
84
89
  }, [isVisible, stateKey, syncedTabs, selectedTabKey]);
@@ -88,6 +93,7 @@ export function OpenAPITabs(
88
93
  return (
89
94
  <OpenAPITabsContext.Provider value={contextValue}>
90
95
  <Tabs
96
+ ref={ref}
91
97
  className="openapi-tabs"
92
98
  onSelectionChange={handleSelectionChange}
93
99
  selectedKey={selectedTab?.key}
@@ -4,8 +4,8 @@ import { ApiClientModalProvider, useApiClientModal } from '@scalar/api-client-re
4
4
  import { useEffect, useImperativeHandle, useRef, useState } from 'react';
5
5
  import { createPortal } from 'react-dom';
6
6
 
7
- import { useOpenAPIOperationContext } from './OpenAPIOperationContext';
8
7
  import { useEventCallback } from 'usehooks-ts';
8
+ import { useOpenAPIOperationContext } from './OpenAPIOperationContext';
9
9
 
10
10
  /**
11
11
  * Button which launches the Scalar API Client
@@ -48,7 +48,7 @@ export function ScalarApiButton({
48
48
  path={path}
49
49
  specUrl={specUrl}
50
50
  />,
51
- document.body,
51
+ document.body
52
52
  )}
53
53
  </div>
54
54
  );
@@ -88,7 +88,7 @@ function ScalarModalController(props: {
88
88
  useImperativeHandle(
89
89
  props.controllerRef,
90
90
  () => ({ openClient: openClient ? () => openClient() : undefined }),
91
- [openClient],
91
+ [openClient]
92
92
  );
93
93
 
94
94
  // Open the client when the component is mounted.