diff --git a/packages/docusaurus-theme-openapi-docs/src/theme-classic.d.ts b/packages/docusaurus-theme-openapi-docs/src/theme-classic.d.ts
index dc7853700..11ea09acc 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme-classic.d.ts
+++ b/packages/docusaurus-theme-openapi-docs/src/theme-classic.d.ts
@@ -6,99 +6,3 @@
* ========================================================================== */
///
-
-declare module "@docusaurus/theme-common/internal" {
- import { CSSProperties, ReactNode, RefObject } from "react";
-
- import type { PropDocContent } from "@docusaurus/plugin-content-docs";
- import { MagicCommentConfig } from "@docusaurus/theme-common/lib/utils/codeBlockUtils";
- import {
- TabsProps as ITabsProps,
- TabValue,
- } from "@docusaurus/theme-common/lib/utils/tabsUtils";
- import { Props as ICodeBlockProps } from "@theme/CodeBlock";
- import { Props as ICopyButtonProps } from "@theme/CodeBlock/CopyButton";
- import { Props as ILineProps } from "@theme/CodeBlock/Line";
- import { PrismTheme } from "prism-react-renderer";
-
- export interface TabItemProps {
- readonly children: ReactNode;
- readonly value: string;
- readonly default?: boolean;
- readonly label?: string;
- readonly className?: string;
- readonly attributes?: { [key: string]: unknown };
- }
-
- export interface TabProps extends ITabsProps {
- length?: number;
- }
-
- export interface CopyButtonProps extends ICopyButtonProps {}
- export interface LineProps extends ILineProps {}
- export interface CodeBlockProps extends ICodeBlockProps {}
-
- export function usePrismTheme(): PrismTheme;
-
- export function sanitizeTabsChildren(children: TabProps["children"]);
-
- export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties;
-
- export function parseCodeBlockTitle(metastring?: string): string;
-
- export function parseLanguage(className: string): string | undefined;
-
- export function containsLineNumbers(metastring?: string): boolean;
-
- export function useScrollPositionBlocker(): {
- blockElementScrollPositionUntilNextRender: (el: HTMLElement) => void;
- };
-
- export function DocProvider({
- children,
- content,
- }: {
- children: ReactNode;
- content: PropDocContent;
- });
-
- export function useTabsContextValue(props: TabProps): {
- selectedValue: string;
- selectValue: (value: string) => void;
- tabValues: readonly TabValue[];
- lazy: boolean;
- block: boolean;
- };
-
- export function useTabs(): {
- selectedValue: string;
- selectValue: (value: string) => void;
- tabValues: readonly TabValue[];
- lazy: boolean;
- block: boolean;
- };
-
- export function TabsProvider(props: {
- children: ReactNode;
- value: ReturnType;
- }): ReactNode;
-
- export function parseLines(
- content: string,
- options: {
- metastring: string | undefined;
- language: string | undefined;
- magicComments: MagicCommentConfig[];
- }
- ): {
- lineClassNames: { [lineIndex: number]: string[] };
- code: string;
- };
-
- export function useCodeWordWrap(): {
- readonly codeBlockRef: RefObject;
- readonly isEnabled: boolean;
- readonly isCodeScrollable: boolean;
- readonly toggle: () => void;
- };
-}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx
index 68f8dffb2..acf3f8691 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx
@@ -8,9 +8,10 @@
import React, { ComponentProps } from "react";
import { ThemeClassNames, usePrismTheme } from "@docusaurus/theme-common";
-import { getPrismCssVariables } from "@docusaurus/theme-common/internal";
import clsx from "clsx";
+import { getPrismCssVariables } from "@theme/utils/codeBlockUtils";
+
export default function CodeBlockContainer({
as: As,
...props
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx
index 5c0c1791c..fff575b7c 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx
@@ -8,13 +8,6 @@
import React from "react";
import { useThemeConfig, usePrismTheme } from "@docusaurus/theme-common";
-import {
- parseCodeBlockTitle,
- parseLanguage,
- parseLines,
- containsLineNumbers,
- useCodeWordWrap,
-} from "@docusaurus/theme-common/internal";
import Container from "@theme/ApiExplorer/ApiCodeBlock/Container";
import CopyButton from "@theme/ApiExplorer/ApiCodeBlock/CopyButton";
import ExpandButton from "@theme/ApiExplorer/ApiCodeBlock/ExpandButton";
@@ -24,6 +17,14 @@ import type { Props } from "@theme/CodeBlock/Content/String";
import clsx from "clsx";
import { Highlight, Language } from "prism-react-renderer";
+import {
+ containsLineNumbers,
+ parseCodeBlockTitle,
+ parseLanguage,
+ parseLines,
+} from "@theme/utils/codeBlockUtils";
+import { useCodeWordWrap } from "@theme/utils/useCodeWordWrap";
+
export default function CodeBlockString({
children,
className: blockClassName = "",
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx
index 94cff38db..e7cc594b6 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx
@@ -7,7 +7,7 @@
import React from "react";
-import { LineProps } from "@docusaurus/theme-common/internal";
+import type { Props as LineProps } from "@theme/CodeBlock/Line";
import clsx from "clsx";
export default function CodeBlockLine({
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx
index b09c2dedd..cec02e9e8 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx
@@ -7,8 +7,8 @@
import React, { isValidElement, ReactNode } from "react";
-import { CodeBlockProps } from "@docusaurus/theme-common/internal";
import useIsBrowser from "@docusaurus/useIsBrowser";
+import type { Props as CodeBlockProps } from "@theme/CodeBlock";
import ElementContent from "@theme/ApiExplorer/ApiCodeBlock/Content/Element";
import StringContent from "@theme/ApiExplorer/ApiCodeBlock/Content/String";
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeTabs/index.tsx
index 513361ced..1ce08df77 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeTabs/index.tsx
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeTabs/index.tsx
@@ -7,17 +7,17 @@
import React, { cloneElement, ReactElement, useEffect, useRef } from "react";
+import useIsBrowser from "@docusaurus/useIsBrowser";
+import clsx from "clsx";
+
+import { useScrollPositionBlocker } from "@theme/utils/scrollUtils";
import {
sanitizeTabsChildren,
type TabItemProps,
type TabProps,
TabsProvider,
- useScrollPositionBlocker,
useTabsContextValue,
-} from "@docusaurus/theme-common/internal";
-import useIsBrowser from "@docusaurus/useIsBrowser";
-import clsx from "clsx";
-
+} from "@theme/utils/tabsUtils";
import { Language } from "../CodeSnippets/code-snippets-types";
export interface Props {
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiTabs/index.tsx
index 86fdd78c0..ff82d82f1 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiTabs/index.tsx
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiTabs/index.tsx
@@ -13,18 +13,19 @@ import React, {
ReactElement,
} from "react";
+import { translate } from "@docusaurus/Translate";
+import useIsBrowser from "@docusaurus/useIsBrowser";
+import Heading from "@theme/Heading";
+import clsx from "clsx";
+
+import { useScrollPositionBlocker } from "@theme/utils/scrollUtils";
import {
sanitizeTabsChildren,
type TabItemProps,
TabProps,
TabsProvider,
- useScrollPositionBlocker,
useTabsContextValue,
-} from "@docusaurus/theme-common/internal";
-import { translate } from "@docusaurus/Translate";
-import useIsBrowser from "@docusaurus/useIsBrowser";
-import Heading from "@theme/Heading";
-import clsx from "clsx";
+} from "@theme/utils/tabsUtils";
export interface TabListProps extends TabProps {
label: string;
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/DiscriminatorTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/DiscriminatorTabs/index.tsx
index a4108e548..99da7a7f6 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme/DiscriminatorTabs/index.tsx
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/DiscriminatorTabs/index.tsx
@@ -13,17 +13,18 @@ import React, {
ReactElement,
} from "react";
+import useIsBrowser from "@docusaurus/useIsBrowser";
+import clsx from "clsx";
+import flatten from "lodash/flatten";
+
+import { useScrollPositionBlocker } from "@theme/utils/scrollUtils";
import {
sanitizeTabsChildren,
type TabItemProps,
TabProps,
TabsProvider,
- useScrollPositionBlocker,
useTabsContextValue,
-} from "@docusaurus/theme-common/internal";
-import useIsBrowser from "@docusaurus/useIsBrowser";
-import clsx from "clsx";
-import flatten from "lodash/flatten";
+} from "@theme/utils/tabsUtils";
function TabList({
className,
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/MimeTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/MimeTabs/index.tsx
index 74841b12b..0feb2bb84 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme/MimeTabs/index.tsx
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/MimeTabs/index.tsx
@@ -13,14 +13,6 @@ import React, {
ReactElement,
} from "react";
-import {
- sanitizeTabsChildren,
- type TabItemProps,
- TabProps,
- TabsProvider,
- useScrollPositionBlocker,
- useTabsContextValue,
-} from "@docusaurus/theme-common/internal";
import useIsBrowser from "@docusaurus/useIsBrowser";
import { setAccept } from "@theme/ApiExplorer/Accept/slice";
import { setContentType } from "@theme/ApiExplorer/ContentType/slice";
@@ -28,6 +20,15 @@ import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
import { RootState } from "@theme/ApiItem/store";
import clsx from "clsx";
+import { useScrollPositionBlocker } from "@theme/utils/scrollUtils";
+import {
+ sanitizeTabsChildren,
+ type TabItemProps,
+ TabProps,
+ TabsProvider,
+ useTabsContextValue,
+} from "@theme/utils/tabsUtils";
+
export interface Props {
schemaType: any;
}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/OperationTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/OperationTabs/index.tsx
index 0c149d668..731e4f1c1 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme/OperationTabs/index.tsx
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/OperationTabs/index.tsx
@@ -13,16 +13,17 @@ import React, {
ReactElement,
} from "react";
+import useIsBrowser from "@docusaurus/useIsBrowser";
+import clsx from "clsx";
+
+import { useScrollPositionBlocker } from "@theme/utils/scrollUtils";
import {
sanitizeTabsChildren,
type TabItemProps,
TabProps,
TabsProvider,
- useScrollPositionBlocker,
useTabsContextValue,
-} from "@docusaurus/theme-common/internal";
-import useIsBrowser from "@docusaurus/useIsBrowser";
-import clsx from "clsx";
+} from "@theme/utils/tabsUtils";
function TabList({
className,
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx
index 85de86472..838c785de 100644
--- a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaTabs/index.tsx
@@ -14,17 +14,18 @@ import React, {
LegacyRef,
} from "react";
+import useIsBrowser from "@docusaurus/useIsBrowser";
+import clsx from "clsx";
+import flatten from "lodash/flatten";
+
+import { useScrollPositionBlocker } from "@theme/utils/scrollUtils";
import {
sanitizeTabsChildren,
type TabItemProps,
TabProps,
TabsProvider,
- useScrollPositionBlocker,
useTabsContextValue,
-} from "@docusaurus/theme-common/internal";
-import useIsBrowser from "@docusaurus/useIsBrowser";
-import clsx from "clsx";
-import flatten from "lodash/flatten";
+} from "@theme/utils/tabsUtils";
export interface SchemaTabsProps extends TabProps {
/**
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/index.tsx
new file mode 100644
index 000000000..eebb4ca1b
--- /dev/null
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/index.tsx
@@ -0,0 +1,61 @@
+/* ============================================================================
+ * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
+ * Portions Copyright (c) Palo Alto Networks
+ *
+ * Swizzled from @docusaurus/theme-classic/src/theme/TabItem/index.tsx (MIT).
+ * Re-points useTabs to our vendored tabsUtils so that reads the same
+ * context our swizzled and OpenAPI tab variants (ApiTabs, MimeTabs,
+ * SchemaTabs, etc.) provide. See:
+ * https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ * ========================================================================== */
+
+import React, { type ReactNode } from "react";
+
+import clsx from "clsx";
+
+import { type TabItemProps, useTabs } from "@theme/utils/tabsUtils";
+
+type Props = TabItemProps;
+import styles from "./styles.module.css";
+
+function TabItemPanel({
+ children,
+ className,
+ hidden,
+}: {
+ children: ReactNode;
+ className?: string;
+ hidden?: boolean;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default function TabItem({
+ children,
+ className,
+ value,
+}: Props): ReactNode {
+ const { selectedValue, lazy } = useTabs();
+ const isSelected = value === selectedValue;
+
+ if (!isSelected && lazy) {
+ return null;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/styles.module.css b/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/styles.module.css
new file mode 100644
index 000000000..f448b7fcd
--- /dev/null
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/TabItem/styles.module.css
@@ -0,0 +1,3 @@
+.tabItem > *:last-child {
+ margin-bottom: 0;
+}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/index.tsx
new file mode 100644
index 000000000..0f0b44517
--- /dev/null
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/index.tsx
@@ -0,0 +1,164 @@
+/* ============================================================================
+ * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
+ * Portions Copyright (c) Palo Alto Networks
+ *
+ * Swizzled from @docusaurus/theme-classic/src/theme/Tabs/index.tsx (MIT).
+ * Re-points the internal hooks (useTabs, useTabsContextValue, etc.) to our
+ * vendored tabsUtils so that the entire / pair runs through a
+ * single React context owned by this package. See:
+ * https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ * ========================================================================== */
+
+import React, { type ReactNode } from "react";
+
+import { ThemeClassNames } from "@docusaurus/theme-common";
+import useIsBrowser from "@docusaurus/useIsBrowser";
+import clsx from "clsx";
+
+import { useScrollPositionBlocker } from "@theme/utils/scrollUtils";
+import {
+ sanitizeTabsChildren,
+ type TabsProps,
+ TabsProvider,
+ useTabs,
+ useTabsContextValue,
+} from "@theme/utils/tabsUtils";
+import styles from "./styles.module.css";
+
+type Props = TabsProps;
+
+function TabList({ className }: { className?: string }) {
+ const { selectedValue, selectValue, tabValues, block } = useTabs();
+
+ const tabRefs: (HTMLLIElement | null)[] = [];
+ const { blockElementScrollPositionUntilNextRender } =
+ useScrollPositionBlocker();
+
+ const handleTabChange = (
+ event:
+ | React.FocusEvent
+ | React.MouseEvent
+ | React.KeyboardEvent
+ ) => {
+ const newTab = event.currentTarget;
+ const newTabIndex = tabRefs.indexOf(newTab);
+ const newTabValue = tabValues[newTabIndex]!.value;
+
+ if (newTabValue !== selectedValue) {
+ blockElementScrollPositionUntilNextRender(newTab);
+ selectValue(newTabValue);
+ }
+ };
+
+ const handleKeydown = (event: React.KeyboardEvent) => {
+ let focusElement: HTMLLIElement | null = null;
+
+ switch (event.key) {
+ case "Enter": {
+ handleTabChange(event);
+ break;
+ }
+ case "ArrowRight": {
+ const nextTab = tabRefs.indexOf(event.currentTarget) + 1;
+ focusElement = tabRefs[nextTab] ?? tabRefs[0]!;
+ break;
+ }
+ case "ArrowLeft": {
+ const prevTab = tabRefs.indexOf(event.currentTarget) - 1;
+ focusElement = tabRefs[prevTab] ?? tabRefs[tabRefs.length - 1]!;
+ break;
+ }
+ default:
+ break;
+ }
+
+ focusElement?.focus();
+ };
+
+ return (
+
+ {tabValues.map(({ value, label, attributes }) => (
+ - {
+ tabRefs.push(ref);
+ }}
+ onKeyDown={handleKeydown}
+ onClick={handleTabChange}
+ {...attributes}
+ className={clsx(
+ "tabs__item",
+ styles.tabItem,
+ attributes?.className as string,
+ {
+ "tabs__item--active": selectedValue === value,
+ }
+ )}
+ >
+ {label ?? value}
+
+ ))}
+
+ );
+}
+
+function TabContent({ children }: { children: ReactNode }) {
+ return {children}
;
+}
+
+function TabsContainer({
+ className,
+ children,
+}: {
+ className?: string;
+ children: ReactNode;
+}): ReactNode {
+ return (
+
+
+ {children}
+
+ );
+}
+
+export default function Tabs(props: Props): ReactNode {
+ const isBrowser = useIsBrowser();
+ const value = useTabsContextValue(props);
+ return (
+
+
+ {sanitizeTabsChildren(props.children)}
+
+
+ );
+}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/styles.module.css b/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/styles.module.css
new file mode 100644
index 000000000..0c79270e7
--- /dev/null
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/Tabs/styles.module.css
@@ -0,0 +1,7 @@
+.tabList {
+ margin-bottom: var(--ifm-leading);
+}
+
+.tabItem {
+ margin-top: 0 !important;
+}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/codeBlockUtils.ts b/packages/docusaurus-theme-openapi-docs/src/theme/utils/codeBlockUtils.ts
new file mode 100644
index 000000000..df830d5dc
--- /dev/null
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/codeBlockUtils.ts
@@ -0,0 +1,296 @@
+/* ============================================================================
+ * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
+ * Portions Copyright (c) Palo Alto Networks
+ *
+ * Vendored subset of @docusaurus/theme-common/src/utils/codeBlockUtils.tsx (MIT)
+ * to remove the dependency on @docusaurus/theme-common/internal.
+ * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ * ========================================================================== */
+
+import type { CSSProperties } from "react";
+
+import rangeParser from "parse-numeric-range";
+import type { PrismTheme, PrismThemeEntry } from "prism-react-renderer";
+
+const codeBlockTitleRegex = /title=(?["'])(?.*?)\1/;
+const metastringLinesRangeRegex = /\{(?[\d,-]+)\}/;
+
+const popularCommentPatterns = {
+ js: { start: "\\/\\/", end: "" },
+ jsBlock: { start: "\\/\\*", end: "\\*\\/" },
+ jsx: { start: "\\{\\s*\\/\\*", end: "\\*\\/\\s*\\}" },
+ bash: { start: "#", end: "" },
+ html: { start: "" },
+} as const;
+
+const commentPatterns = {
+ ...popularCommentPatterns,
+ lua: { start: "--", end: "" },
+ wasm: { start: "\\;\\;", end: "" },
+ tex: { start: "%", end: "" },
+ vb: { start: "['‘’]", end: "" },
+ vbnet: { start: "(?:_\\s*)?['‘’]", end: "" },
+ rem: { start: "[Rr][Ee][Mm]\\b", end: "" },
+ f90: { start: "!", end: "" },
+ ml: { start: "\\(\\*", end: "\\*\\)" },
+ cobol: { start: "\\*>", end: "" },
+} as const;
+
+type CommentType = keyof typeof commentPatterns;
+const popularCommentTypes = Object.keys(
+ popularCommentPatterns
+) as CommentType[];
+
+export type MagicCommentConfig = {
+ className: string;
+ line?: string;
+ block?: { start: string; end: string };
+};
+
+function getCommentPattern(
+ languages: CommentType[],
+ magicCommentDirectives: MagicCommentConfig[]
+) {
+ const commentPattern = languages
+ .map((lang) => {
+ const { start, end } = commentPatterns[lang];
+ return `(?:${start}\\s*(${magicCommentDirectives
+ .flatMap((d) => [d.line, d.block?.start, d.block?.end].filter(Boolean))
+ .join("|")})\\s*${end})`;
+ })
+ .join("|");
+ return new RegExp(`^\\s*(?:${commentPattern})\\s*$`);
+}
+
+function getAllMagicCommentDirectiveStyles(
+ lang: string,
+ magicCommentDirectives: MagicCommentConfig[]
+) {
+ switch (lang) {
+ case "js":
+ case "javascript":
+ case "ts":
+ case "typescript":
+ return getCommentPattern(["js", "jsBlock"], magicCommentDirectives);
+
+ case "jsx":
+ case "tsx":
+ return getCommentPattern(
+ ["js", "jsBlock", "jsx"],
+ magicCommentDirectives
+ );
+
+ case "html":
+ return getCommentPattern(
+ ["js", "jsBlock", "html"],
+ magicCommentDirectives
+ );
+
+ case "python":
+ case "py":
+ case "bash":
+ return getCommentPattern(["bash"], magicCommentDirectives);
+
+ case "markdown":
+ case "md":
+ return getCommentPattern(["html", "jsx", "bash"], magicCommentDirectives);
+
+ case "tex":
+ case "latex":
+ case "matlab":
+ return getCommentPattern(["tex"], magicCommentDirectives);
+
+ case "lua":
+ case "haskell":
+ return getCommentPattern(["lua"], magicCommentDirectives);
+
+ case "sql":
+ return getCommentPattern(["lua", "jsBlock"], magicCommentDirectives);
+
+ case "wasm":
+ return getCommentPattern(["wasm"], magicCommentDirectives);
+
+ case "vb":
+ case "vba":
+ case "visual-basic":
+ return getCommentPattern(["vb", "rem"], magicCommentDirectives);
+ case "vbnet":
+ return getCommentPattern(["vbnet", "rem"], magicCommentDirectives);
+
+ case "batch":
+ return getCommentPattern(["rem"], magicCommentDirectives);
+
+ case "basic":
+ return getCommentPattern(["rem", "f90"], magicCommentDirectives);
+
+ case "fsharp":
+ return getCommentPattern(["js", "ml"], magicCommentDirectives);
+
+ case "ocaml":
+ case "sml":
+ return getCommentPattern(["ml"], magicCommentDirectives);
+
+ case "fortran":
+ return getCommentPattern(["f90"], magicCommentDirectives);
+
+ case "cobol":
+ return getCommentPattern(["cobol"], magicCommentDirectives);
+
+ default:
+ return getCommentPattern(popularCommentTypes, magicCommentDirectives);
+ }
+}
+
+export function parseCodeBlockTitle(metastring?: string): string {
+ return metastring?.match(codeBlockTitleRegex)?.groups!.title ?? "";
+}
+
+export function containsLineNumbers(metastring?: string): boolean {
+ return Boolean(metastring?.includes("showLineNumbers"));
+}
+
+type ParseCodeLinesParam = {
+ metastring: string | undefined;
+ language: string | undefined;
+ magicComments: MagicCommentConfig[];
+};
+
+type CodeLineClassNames = { [lineIndex: number]: string[] };
+
+type ParsedCodeLines = {
+ code: string;
+ lineClassNames: CodeLineClassNames;
+};
+
+function parseCodeLinesFromMetastring(
+ code: string,
+ { metastring, magicComments }: ParseCodeLinesParam
+): ParsedCodeLines | null {
+ if (metastring && metastringLinesRangeRegex.test(metastring)) {
+ const linesRange = metastring.match(metastringLinesRangeRegex)!.groups!
+ .range!;
+ if (magicComments.length === 0) {
+ throw new Error(
+ `A highlight range has been given in code block's metastring (\`\`\` ${metastring}), but no magic comment config is available. Docusaurus applies the first magic comment entry's className for metastring ranges.`
+ );
+ }
+ const metastringRangeClassName = magicComments[0]!.className;
+ const lines = rangeParser(linesRange)
+ .filter((n) => n > 0)
+ .map((n) => [n - 1, [metastringRangeClassName]] as [number, string[]]);
+ return { lineClassNames: Object.fromEntries(lines), code };
+ }
+ return null;
+}
+
+function parseCodeLinesFromContent(
+ code: string,
+ params: ParseCodeLinesParam
+): ParsedCodeLines {
+ const { language, magicComments } = params;
+ if (language === undefined) {
+ return { lineClassNames: {}, code };
+ }
+ const directiveRegex = getAllMagicCommentDirectiveStyles(
+ language,
+ magicComments
+ );
+ const lines = code.split(/\r?\n/);
+ const blocks = Object.fromEntries(
+ magicComments.map((d) => [d.className, { start: 0, range: "" }])
+ );
+ const lineToClassName: { [comment: string]: string } = Object.fromEntries(
+ magicComments
+ .filter((d) => d.line)
+ .map(({ className, line }) => [line!, className] as [string, string])
+ );
+ const blockStartToClassName: { [comment: string]: string } =
+ Object.fromEntries(
+ magicComments
+ .filter((d) => d.block)
+ .map(({ className, block }) => [block!.start, className])
+ );
+ const blockEndToClassName: { [comment: string]: string } = Object.fromEntries(
+ magicComments
+ .filter((d) => d.block)
+ .map(({ className, block }) => [block!.end, className])
+ );
+ for (let lineNumber = 0; lineNumber < lines.length; ) {
+ const line = lines[lineNumber]!;
+ const match = line.match(directiveRegex);
+ if (!match) {
+ lineNumber += 1;
+ continue;
+ }
+ const directive = match
+ .slice(1)
+ .find((item: string | undefined) => item !== undefined)!;
+ if (lineToClassName[directive]) {
+ blocks[lineToClassName[directive]!]!.range += `${lineNumber},`;
+ } else if (blockStartToClassName[directive]) {
+ blocks[blockStartToClassName[directive]!]!.start = lineNumber;
+ } else if (blockEndToClassName[directive]) {
+ blocks[blockEndToClassName[directive]!]!.range += `${
+ blocks[blockEndToClassName[directive]!]!.start
+ }-${lineNumber - 1},`;
+ }
+ lines.splice(lineNumber, 1);
+ }
+
+ const lineClassNames: { [lineIndex: number]: string[] } = {};
+ Object.entries(blocks).forEach(([className, { range }]) => {
+ rangeParser(range).forEach((l) => {
+ lineClassNames[l] ??= [];
+ lineClassNames[l]!.push(className);
+ });
+ });
+
+ return { code: lines.join("\n"), lineClassNames };
+}
+
+export function parseLines(
+ code: string,
+ params: ParseCodeLinesParam
+): ParsedCodeLines {
+ const newCode = code.replace(/\r?\n$/, "");
+ return (
+ parseCodeLinesFromMetastring(newCode, { ...params }) ??
+ parseCodeLinesFromContent(newCode, { ...params })
+ );
+}
+
+function parseClassNameLanguage(
+ className: string | undefined
+): string | undefined {
+ if (!className) {
+ return undefined;
+ }
+ const languageClassName = className
+ .split(" ")
+ .find((str) => str.startsWith("language-"));
+ return languageClassName?.replace(/language-/, "");
+}
+
+// Upstream renamed `parseLanguage` to `parseClassNameLanguage`; the back-compat
+// shim in @docusaurus/theme-common/internal re-exports it under the old name.
+// We keep the old name here since our call sites still use it.
+export { parseClassNameLanguage as parseLanguage };
+
+export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
+ const mapping: PrismThemeEntry = {
+ color: "--prism-color",
+ backgroundColor: "--prism-background-color",
+ };
+
+ const properties: { [key: string]: string } = {};
+ Object.entries(prismTheme.plain).forEach(([key, value]) => {
+ const varName = mapping[key as keyof PrismThemeEntry];
+ if (varName && typeof value === "string") {
+ properties[varName] = value;
+ }
+ });
+ return properties;
+}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/reactUtils.ts b/packages/docusaurus-theme-openapi-docs/src/theme/utils/reactUtils.ts
new file mode 100644
index 000000000..0a2edd37c
--- /dev/null
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/reactUtils.ts
@@ -0,0 +1,48 @@
+/* ============================================================================
+ * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
+ * Portions Copyright (c) Palo Alto Networks
+ *
+ * Vendored subset of @docusaurus/theme-common/src/utils/reactUtils.tsx (MIT)
+ * to remove the dependency on @docusaurus/theme-common/internal, which is
+ * scheduled for removal in Docusaurus v4.
+ * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ * ========================================================================== */
+
+import { useCallback, useMemo, useRef } from "react";
+
+import useIsomorphicLayoutEffect from "@docusaurus/useIsomorphicLayoutEffect";
+
+export function useEvent unknown>(
+ callback: T
+): T {
+ const ref = useRef(callback);
+
+ useIsomorphicLayoutEffect(() => {
+ ref.current = callback;
+ }, [callback]);
+
+ // @ts-expect-error: TS is right that this callback may be a supertype of T,
+ // but good enough for our use
+ return useCallback((...args) => ref.current(...args), []);
+}
+
+export function useShallowMemoObject(obj: O): O {
+ const deps = Object.entries(obj);
+ deps.sort((a, b) => a[0].localeCompare(b[0]));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ return useMemo(() => obj, deps.flat());
+}
+
+export class ReactContextError extends Error {
+ constructor(providerName: string, additionalInfo?: string) {
+ super();
+ this.name = "ReactContextError";
+ this.message = `Hook ${
+ this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?\w+)/)?.groups!
+ .name ?? ""
+ } is called outside the <${providerName}>. ${additionalInfo ?? ""}`;
+ }
+}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/scrollUtils.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/utils/scrollUtils.tsx
new file mode 100644
index 000000000..6490a2ca6
--- /dev/null
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/scrollUtils.tsx
@@ -0,0 +1,154 @@
+/* ============================================================================
+ * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
+ * Portions Copyright (c) Palo Alto Networks
+ *
+ * Vendored subset of @docusaurus/theme-common/src/utils/scrollUtils.tsx (MIT)
+ * to remove the dependency on @docusaurus/theme-common/internal. Only the
+ * ScrollControllerProvider + useScrollPositionBlocker surface is kept, since
+ * that's all our tab renderers need. The ScrollControllerProvider must be
+ * mounted in the React tree above any consumer of useScrollPositionBlocker
+ * (see ApiItem/index.tsx).
+ * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ * ========================================================================== */
+
+import React, {
+ useCallback,
+ useContext,
+ useMemo,
+ useRef,
+ type ReactNode,
+} from "react";
+
+import useIsomorphicLayoutEffect from "@docusaurus/useIsomorphicLayoutEffect";
+
+import { ReactContextError } from "./reactUtils";
+
+type ScrollController = {
+ scrollEventsEnabledRef: React.RefObject;
+ enableScrollEvents: () => void;
+ disableScrollEvents: () => void;
+};
+
+function useScrollControllerContextValue(): ScrollController {
+ const scrollEventsEnabledRef = useRef(true);
+
+ return useMemo(
+ () => ({
+ scrollEventsEnabledRef,
+ enableScrollEvents: () => {
+ scrollEventsEnabledRef.current = true;
+ },
+ disableScrollEvents: () => {
+ scrollEventsEnabledRef.current = false;
+ },
+ }),
+ []
+ );
+}
+
+const ScrollMonitorContext = React.createContext(
+ undefined
+);
+
+export function ScrollControllerProvider({
+ children,
+}: {
+ children: ReactNode;
+}): ReactNode {
+ const value = useScrollControllerContextValue();
+ return (
+
+ {children}
+
+ );
+}
+
+function useScrollController(): ScrollController {
+ const context = useContext(ScrollMonitorContext);
+ if (context == null) {
+ throw new ReactContextError("ScrollControllerProvider");
+ }
+ return context;
+}
+
+type UseScrollPositionSaver = {
+ save: (elem: HTMLElement) => void;
+ restore: () => { restored: boolean };
+};
+
+function useScrollPositionSaver(): UseScrollPositionSaver {
+ const lastElementRef = useRef<{ elem: HTMLElement | null; top: number }>({
+ elem: null,
+ top: 0,
+ });
+
+ const save = useCallback((elem: HTMLElement) => {
+ lastElementRef.current = {
+ elem,
+ top: elem.getBoundingClientRect().top,
+ };
+ }, []);
+
+ const restore = useCallback(() => {
+ const {
+ current: { elem, top },
+ } = lastElementRef;
+ if (!elem) {
+ return { restored: false };
+ }
+ const newTop = elem.getBoundingClientRect().top;
+ const heightDiff = newTop - top;
+ if (heightDiff) {
+ window.scrollBy({ left: 0, top: heightDiff });
+ }
+ lastElementRef.current = { elem: null, top: 0 };
+
+ return { restored: heightDiff !== 0 };
+ }, []);
+
+ return useMemo(() => ({ save, restore }), [restore, save]);
+}
+
+export function useScrollPositionBlocker(): {
+ blockElementScrollPositionUntilNextRender: (el: HTMLElement) => void;
+} {
+ const scrollController = useScrollController();
+ const scrollPositionSaver = useScrollPositionSaver();
+
+ const nextLayoutEffectCallbackRef = useRef<(() => void) | undefined>(
+ undefined
+ );
+
+ const blockElementScrollPositionUntilNextRender = useCallback(
+ (el: HTMLElement) => {
+ scrollPositionSaver.save(el);
+ scrollController.disableScrollEvents();
+ nextLayoutEffectCallbackRef.current = () => {
+ const { restored } = scrollPositionSaver.restore();
+ nextLayoutEffectCallbackRef.current = undefined;
+
+ if (restored) {
+ const handleScrollRestoreEvent = () => {
+ scrollController.enableScrollEvents();
+ window.removeEventListener("scroll", handleScrollRestoreEvent);
+ };
+ window.addEventListener("scroll", handleScrollRestoreEvent);
+ } else {
+ scrollController.enableScrollEvents();
+ }
+ };
+ },
+ [scrollController, scrollPositionSaver]
+ );
+
+ useIsomorphicLayoutEffect(() => {
+ queueMicrotask(() => nextLayoutEffectCallbackRef.current?.());
+ });
+
+ return {
+ blockElementScrollPositionUntilNextRender,
+ };
+}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/tabsUtils.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/utils/tabsUtils.tsx
new file mode 100644
index 000000000..0ed562496
--- /dev/null
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/tabsUtils.tsx
@@ -0,0 +1,329 @@
+/* ============================================================================
+ * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
+ * Portions Copyright (c) Palo Alto Networks
+ *
+ * Vendored from @docusaurus/theme-common/src/utils/tabsUtils.tsx (MIT) to
+ * remove the dependency on @docusaurus/theme-common/internal. The
+ * useQueryStringValue dependency from theme-common's historyUtils is inlined
+ * below to avoid pulling another internal module.
+ * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ * ========================================================================== */
+
+import React, {
+ createContext,
+ isValidElement,
+ useCallback,
+ useMemo,
+ useState,
+ useSyncExternalStore,
+ type ReactElement,
+ type ReactNode,
+} from "react";
+
+import { useHistory } from "@docusaurus/router";
+import { duplicates, useStorageSlot } from "@docusaurus/theme-common";
+import useIsomorphicLayoutEffect from "@docusaurus/useIsomorphicLayoutEffect";
+
+import { ScrollControllerProvider } from "./scrollUtils";
+
+export interface TabValue {
+ readonly value: string;
+ readonly label?: string;
+ readonly attributes?: { [key: string]: unknown };
+ readonly default?: boolean;
+}
+
+export interface TabsProps {
+ readonly lazy?: boolean;
+ readonly block?: boolean;
+ readonly children: ReactNode;
+ readonly defaultValue?: string | null;
+ readonly values?: readonly TabValue[];
+ readonly groupId?: string;
+ readonly className?: string;
+ readonly queryString?: string | boolean;
+}
+
+// Extended Tabs type used across the OpenAPI theme; preserves the historical
+// shape that included an optional `length` field.
+export interface TabProps extends TabsProps {
+ length?: number;
+}
+
+export interface TabItemProps {
+ readonly children: ReactNode;
+ readonly value: string;
+ readonly default?: boolean;
+ readonly label?: string;
+ readonly className?: string;
+ readonly attributes?: { [key: string]: unknown };
+}
+
+export function sanitizeTabsChildren(children: ReactNode): ReactNode {
+ return React.Children.toArray(children).filter((child) => child !== "\n");
+}
+
+function extractChildrenTabValues(children: ReactNode): TabValue[] {
+ function isTabItemWithValueProp(
+ comp: ReactElement
+ ): comp is ReactElement {
+ const { props } = comp;
+ return !!props && typeof props === "object" && "value" in props;
+ }
+
+ const elements = React.Children.toArray(children).flatMap((child) => {
+ if (!child) {
+ return [];
+ }
+ if (isValidElement(child) && isTabItemWithValueProp(child)) {
+ return [child];
+ }
+ const badChildTypeName =
+ // @ts-expect-error: guarding against unexpected cases
+ typeof child.type === "string" ? child.type : child.type.name;
+ throw new Error(
+ `Docusaurus error: Bad child <${badChildTypeName}>: all children of the component should be , and every should have a unique "value" prop.
+If you do not want to pass on a "value" prop to the direct children of , you can also pass an explicit prop.`
+ );
+ });
+
+ return elements.map(
+ ({ props: { value, label, attributes, default: isDefault } }) => ({
+ value,
+ label,
+ attributes,
+ default: isDefault,
+ })
+ );
+}
+
+function ensureNoDuplicateValue(values: readonly TabValue[]) {
+ const dup = duplicates(values, (a, b) => a.value === b.value);
+ if (dup.length > 0) {
+ throw new Error(
+ `Docusaurus error: Duplicate values "${dup
+ .map((a) => `'${a.value}'`)
+ .join(", ")}" found in . Every value needs to be unique.`
+ );
+ }
+}
+
+function useTabValues(
+ props: Pick
+): readonly TabValue[] {
+ const { values: valuesProp, children } = props;
+ return useMemo(() => {
+ const values = valuesProp ?? extractChildrenTabValues(children);
+ ensureNoDuplicateValue(values);
+ return values;
+ }, [valuesProp, children]);
+}
+
+function isValidValue({
+ value,
+ tabValues,
+}: {
+ value: string | null | undefined;
+ tabValues: readonly TabValue[];
+}) {
+ return tabValues.some((a) => a.value === value);
+}
+
+function getInitialStateValue({
+ defaultValue,
+ tabValues,
+}: {
+ defaultValue: TabsProps["defaultValue"];
+ tabValues: readonly TabValue[];
+}): string {
+ if (tabValues.length === 0) {
+ throw new Error(
+ "Docusaurus error: the component requires at least one children component"
+ );
+ }
+ if (defaultValue) {
+ if (!isValidValue({ value: defaultValue, tabValues })) {
+ throw new Error(
+ `Docusaurus error: The has a defaultValue "${defaultValue}" but none of its children has the corresponding value. Available values are: ${tabValues
+ .map((a) => a.value)
+ .join(
+ ", "
+ )}. If you intend to show no default tab, use defaultValue={null} instead.`
+ );
+ }
+ return defaultValue;
+ }
+ const defaultTabValue =
+ tabValues.find((tabValue) => tabValue.default) ?? tabValues[0];
+ if (!defaultTabValue) {
+ throw new Error("Unexpected error: 0 tabValues");
+ }
+ return defaultTabValue.value;
+}
+
+function getStorageKey(groupId: string | undefined) {
+ if (!groupId) {
+ return null;
+ }
+ return `docusaurus.tab.${groupId}`;
+}
+
+function getQueryStringKey({
+ queryString = false,
+ groupId,
+}: Pick) {
+ if (typeof queryString === "string") {
+ return queryString;
+ }
+ if (queryString === false) {
+ return null;
+ }
+ if (queryString === true && !groupId) {
+ throw new Error(
+ `Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".`
+ );
+ }
+ return groupId ?? null;
+}
+
+// Inlined from @docusaurus/theme-common/internal historyUtils.useQueryStringValue
+function useQueryStringValue(key: string | null): string | null {
+ const history = useHistory();
+ return useSyncExternalStore(
+ history.listen,
+ () =>
+ key === null
+ ? null
+ : new URLSearchParams(history.location.search).get(key),
+ () => null
+ );
+}
+
+function useTabQueryString({
+ queryString = false,
+ groupId,
+}: Pick) {
+ const history = useHistory();
+ const key = getQueryStringKey({ queryString, groupId });
+ const value = useQueryStringValue(key);
+
+ const setValue = useCallback(
+ (newValue: string) => {
+ if (!key) {
+ return;
+ }
+ const searchParams = new URLSearchParams(history.location.search);
+ searchParams.set(key, newValue);
+ history.replace({ ...history.location, search: searchParams.toString() });
+ },
+ [key, history]
+ );
+
+ return [value, setValue] as const;
+}
+
+function useTabStorage({ groupId }: Pick) {
+ const key = getStorageKey(groupId);
+ const [value, storageSlot] = useStorageSlot(key);
+
+ const setValue = useCallback(
+ (newValue: string) => {
+ if (!key) {
+ return;
+ }
+ storageSlot.set(newValue);
+ },
+ [key, storageSlot]
+ );
+
+ return [value, setValue] as const;
+}
+
+type TabsContextValue = {
+ selectedValue: string;
+ selectValue: (value: string) => void;
+ tabValues: readonly TabValue[];
+ lazy: boolean;
+ block: boolean;
+};
+
+export function useTabsContextValue(props: TabsProps): TabsContextValue {
+ const { defaultValue, queryString = false, groupId } = props;
+ const tabValues = useTabValues(props);
+
+ const [selectedValue, setSelectedValue] = useState(() =>
+ getInitialStateValue({ defaultValue, tabValues })
+ );
+
+ const [queryStringValue, setQueryString] = useTabQueryString({
+ queryString,
+ groupId,
+ });
+
+ const [storageValue, setStorageValue] = useTabStorage({
+ groupId,
+ });
+
+ const valueToSync = (() => {
+ const value = queryStringValue ?? storageValue;
+ if (!isValidValue({ value, tabValues })) {
+ return null;
+ }
+ return value;
+ })();
+
+ useIsomorphicLayoutEffect(() => {
+ if (valueToSync) {
+ setSelectedValue(valueToSync);
+ }
+ }, [valueToSync]);
+
+ const selectValue = useCallback(
+ (newValue: string) => {
+ if (!isValidValue({ value: newValue, tabValues })) {
+ throw new Error(`Can't select invalid tab value=${newValue}`);
+ }
+ setSelectedValue(newValue);
+ setQueryString(newValue);
+ setStorageValue(newValue);
+ },
+ [setQueryString, setStorageValue, tabValues]
+ );
+
+ return {
+ selectedValue,
+ selectValue,
+ tabValues,
+ lazy: props.lazy ?? false,
+ block: props.block ?? false,
+ };
+}
+
+const TabsContext = createContext(null);
+
+export function useTabs(): TabsContextValue {
+ const contextValue = React.useContext(TabsContext);
+ if (!contextValue) {
+ throw new Error("useTabsContext() must be used within a Tabs component");
+ }
+ return contextValue;
+}
+
+export function TabsProvider(props: {
+ children: ReactNode;
+ value: TabsContextValue;
+}): ReactNode {
+ // ScrollControllerProvider is mounted here so every tab consumer
+ // (our six OpenAPI tab variants + the swizzled @theme/Tabs) gets a working
+ // useScrollPositionBlocker without callers needing a separate provider.
+ return (
+
+
+ {props.children}
+
+
+ );
+}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/useCodeWordWrap.ts b/packages/docusaurus-theme-openapi-docs/src/theme/utils/useCodeWordWrap.ts
new file mode 100644
index 000000000..820f4d801
--- /dev/null
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/useCodeWordWrap.ts
@@ -0,0 +1,110 @@
+/* ============================================================================
+ * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
+ * Portions Copyright (c) Palo Alto Networks
+ *
+ * Vendored from @docusaurus/theme-common/src/hooks/useCodeWordWrap.ts (MIT)
+ * to remove the dependency on @docusaurus/theme-common/internal.
+ * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ * ========================================================================== */
+
+import { useCallback, useEffect, useRef, useState } from "react";
+import type { RefObject } from "react";
+
+import { useMutationObserver } from "./useMutationObserver";
+
+// Callback fires when the "hidden" attribute of a tabpanel changes
+// See https://github.com/facebook/docusaurus/pull/7485
+function useTabBecameVisibleCallback(
+ codeBlockRef: RefObject,
+ callback: () => void
+) {
+ const [hiddenTabElement, setHiddenTabElement] = useState<
+ Element | null | undefined
+ >();
+
+ const updateHiddenTabElement = useCallback(() => {
+ setHiddenTabElement(
+ codeBlockRef.current?.closest("[role=tabpanel][hidden]")
+ );
+ }, [codeBlockRef, setHiddenTabElement]);
+
+ useEffect(() => {
+ updateHiddenTabElement();
+ }, [updateHiddenTabElement]);
+
+ useMutationObserver(
+ hiddenTabElement,
+ (mutations: MutationRecord[]) => {
+ mutations.forEach((mutation) => {
+ if (
+ mutation.type === "attributes" &&
+ mutation.attributeName === "hidden"
+ ) {
+ callback();
+ updateHiddenTabElement();
+ }
+ });
+ },
+ {
+ attributes: true,
+ characterData: false,
+ childList: false,
+ subtree: false,
+ }
+ );
+}
+
+export type WordWrap = {
+ readonly codeBlockRef: RefObject;
+ readonly isEnabled: boolean;
+ readonly isCodeScrollable: boolean;
+ readonly toggle: () => void;
+};
+
+export function useCodeWordWrap(): WordWrap {
+ const [isEnabled, setIsEnabled] = useState(false);
+ const [isCodeScrollable, setIsCodeScrollable] = useState(false);
+ const codeBlockRef = useRef(null);
+
+ const toggle = useCallback(() => {
+ const codeElement = codeBlockRef.current!.querySelector("code")!;
+
+ if (isEnabled) {
+ codeElement.removeAttribute("style");
+ } else {
+ codeElement.style.whiteSpace = "pre-wrap";
+ codeElement.style.overflowWrap = "anywhere";
+ }
+
+ setIsEnabled((value) => !value);
+ }, [codeBlockRef, isEnabled]);
+
+ const updateCodeIsScrollable = useCallback(() => {
+ const { scrollWidth, clientWidth } = codeBlockRef.current!;
+ const isScrollable =
+ scrollWidth > clientWidth ||
+ codeBlockRef.current!.querySelector("code")!.hasAttribute("style");
+ setIsCodeScrollable(isScrollable);
+ }, [codeBlockRef]);
+
+ useTabBecameVisibleCallback(codeBlockRef, updateCodeIsScrollable);
+
+ useEffect(() => {
+ updateCodeIsScrollable();
+ }, [isEnabled, updateCodeIsScrollable]);
+
+ useEffect(() => {
+ window.addEventListener("resize", updateCodeIsScrollable, {
+ passive: true,
+ });
+
+ return () => {
+ window.removeEventListener("resize", updateCodeIsScrollable);
+ };
+ }, [updateCodeIsScrollable]);
+
+ return { codeBlockRef, isEnabled, isCodeScrollable, toggle };
+}
diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/utils/useMutationObserver.ts b/packages/docusaurus-theme-openapi-docs/src/theme/utils/useMutationObserver.ts
new file mode 100644
index 000000000..d81a83c48
--- /dev/null
+++ b/packages/docusaurus-theme-openapi-docs/src/theme/utils/useMutationObserver.ts
@@ -0,0 +1,41 @@
+/* ============================================================================
+ * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
+ * Portions Copyright (c) Palo Alto Networks
+ *
+ * Vendored from @docusaurus/theme-common/src/hooks/useMutationObserver.ts (MIT)
+ * to remove the dependency on @docusaurus/theme-common/internal.
+ * See: https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issues/1140
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ * ========================================================================== */
+
+import { useEffect } from "react";
+
+import { useEvent, useShallowMemoObject } from "./reactUtils";
+
+type Options = MutationObserverInit;
+
+const DefaultOptions: Options = {
+ attributes: true,
+ characterData: true,
+ childList: true,
+ subtree: true,
+};
+
+export function useMutationObserver(
+ target: Element | undefined | null,
+ callback: MutationCallback,
+ options: Options = DefaultOptions
+): void {
+ const stableCallback = useEvent(callback);
+ const stableOptions: Options = useShallowMemoObject(options);
+
+ useEffect(() => {
+ const observer = new MutationObserver(stableCallback);
+ if (target) {
+ observer.observe(target, stableOptions);
+ }
+ return () => observer.disconnect();
+ }, [target, stableCallback, stableOptions]);
+}