From 2e5c98e9fc62ab36ffd24a3226a06e0904db4c5e Mon Sep 17 00:00:00 2001
From: Putu Audi Pasuatmadi <63685606+audipasuatmadi@users.noreply.github.com>
Date: Fri, 8 Nov 2024 12:42:20 +0800
Subject: [PATCH 1/4] Add Virtualization to Redoc (#1)

* add virtualization to redoc

* address code review, simply stuffs

---------

Co-authored-by: Putu Audi Pasuatmadi <audipasuatmadi@users.noreply.github.com>
---
 package-lock.json                             |  39 +++++
 package.json                                  |   1 +
 src/common-elements/panels.ts                 |   1 +
 src/components/Redoc/Redoc.tsx                |   7 +-
 .../Virtualization/VirtualizedContent.tsx     |  91 ++++++++++++
 .../Virtualization/useItemReverseIndex.tsx    |  32 +++++
 .../Virtualization/useSelectedTag.tsx         |  40 ++++++
 .../__snapshots__/FieldDetails.test.tsx.snap  |   6 +-
 .../SecurityRequirement.test.tsx.snap         |  26 ++--
 .../__tests__/useItemReverseIndex.test.tsx    | 100 +++++++++++++
 .../__tests__/useSelectedTag.test.tsx         | 134 ++++++++++++++++++
 11 files changed, 457 insertions(+), 20 deletions(-)
 create mode 100644 src/components/Virtualization/VirtualizedContent.tsx
 create mode 100644 src/components/Virtualization/useItemReverseIndex.tsx
 create mode 100644 src/components/Virtualization/useSelectedTag.tsx
 create mode 100644 src/components/__tests__/useItemReverseIndex.test.tsx
 create mode 100644 src/components/__tests__/useSelectedTag.test.tsx

diff --git a/package-lock.json b/package-lock.json
index 69e5754b34..5e7a92c861 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
       "dependencies": {
         "@cfaester/enzyme-adapter-react-18": "^0.8.0",
         "@redocly/openapi-core": "^1.4.0",
+        "@tanstack/react-virtual": "^3.10.8",
         "classnames": "^2.3.2",
         "decko": "^1.2.0",
         "dompurify": "^3.0.6",
@@ -3623,6 +3624,31 @@
         "size-limit": "11.1.4"
       }
     },
+    "node_modules/@tanstack/react-virtual": {
+      "version": "3.10.8",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz",
+      "integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==",
+      "dependencies": {
+        "@tanstack/virtual-core": "3.10.8"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+        "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      }
+    },
+    "node_modules/@tanstack/virtual-core": {
+      "version": "3.10.8",
+      "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz",
+      "integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      }
+    },
     "node_modules/@tootallnate/once": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -21749,6 +21775,19 @@
       "dev": true,
       "requires": {}
     },
+    "@tanstack/react-virtual": {
+      "version": "3.10.8",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz",
+      "integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==",
+      "requires": {
+        "@tanstack/virtual-core": "3.10.8"
+      }
+    },
+    "@tanstack/virtual-core": {
+      "version": "3.10.8",
+      "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz",
+      "integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA=="
+    },
     "@tootallnate/once": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
diff --git a/package.json b/package.json
index 4c50b8e9ca..7d9687cc57 100644
--- a/package.json
+++ b/package.json
@@ -139,6 +139,7 @@
   "dependencies": {
     "@cfaester/enzyme-adapter-react-18": "^0.8.0",
     "@redocly/openapi-core": "^1.4.0",
+    "@tanstack/react-virtual": "^3.10.8",
     "classnames": "^2.3.2",
     "decko": "^1.2.0",
     "dompurify": "^3.0.6",
diff --git a/src/common-elements/panels.ts b/src/common-elements/panels.ts
index cacdb685c5..7ce949a6a9 100644
--- a/src/common-elements/panels.ts
+++ b/src/common-elements/panels.ts
@@ -62,6 +62,7 @@ export const RightPanel = styled.div`
 
 export const DarkRightPanel = styled(RightPanel)`
   background-color: ${props => props.theme.rightPanel.backgroundColor};
+  border-radius: 0.2rem;
 `;
 
 export const Row = styled.div`
diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx
index a3d0eef7cd..bc12a84d18 100644
--- a/src/components/Redoc/Redoc.tsx
+++ b/src/components/Redoc/Redoc.tsx
@@ -5,15 +5,15 @@ import { ThemeProvider } from '../../styled-components';
 import { OptionsProvider } from '../OptionsProvider';
 
 import { AppStore } from '../../services';
-import { ApiInfo } from '../ApiInfo/';
+
 import { ApiLogo } from '../ApiLogo/ApiLogo';
-import { ContentItems } from '../ContentItems/ContentItems';
 import { SideMenu } from '../SideMenu/SideMenu';
 import { StickyResponsiveSidebar } from '../StickySidebar/StickyResponsiveSidebar';
 import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements';
 
 import { SearchBox } from '../SearchBox/SearchBox';
 import { StoreProvider } from '../StoreBuilder';
+import VirtualizedContent from '../Virtualization/VirtualizedContent';
 
 export interface RedocProps {
   store: AppStore;
@@ -56,8 +56,7 @@ export class Redoc extends React.Component<RedocProps> {
                 <SideMenu menu={menu} />
               </StickyResponsiveSidebar>
               <ApiContentWrap className="api-content">
-                <ApiInfo store={store} />
-                <ContentItems items={menu.items as any} />
+                <VirtualizedContent store={store} menu={menu} />
               </ApiContentWrap>
               <BackgroundStub />
             </RedocWrap>
diff --git a/src/components/Virtualization/VirtualizedContent.tsx b/src/components/Virtualization/VirtualizedContent.tsx
new file mode 100644
index 0000000000..bdf8f86f05
--- /dev/null
+++ b/src/components/Virtualization/VirtualizedContent.tsx
@@ -0,0 +1,91 @@
+import * as React from 'react';
+import { ApiInfo, AppStore, ContentItem, ContentItemModel, MenuStore } from '../..';
+import { useVirtualizer } from '@tanstack/react-virtual';
+import useSelectedTag from './useSelectedTag';
+import useItemReverseIndex from './useItemReverseIndex';
+
+type VirtualizedContentProps = {
+  store: AppStore;
+  menu: MenuStore;
+};
+
+/**
+ * VirtualizedContent optimizes the rendering of API documentation in Redoc by virtualizing the content.
+ *
+ * It ensures that only the API sections currently visible within the user's viewport are rendered,
+ * while off-screen sections remain unloaded until they come into view.
+ * The data is still in the memory, at least the HTML doesn't have to render it which does frees
+ * quite a huge amount of memory.
+ *
+ * This approach prevents memory issues that can arise when rendering large API documentation
+ * by reducing the amount of content loaded into memory at any one time, thereby enhancing
+ * performance and preventing potential crashes due to excessive memory usage.
+ *
+ * @author Audi
+ */
+const VirtualizedContent = ({ store, menu }: VirtualizedContentProps) => {
+  const scrollableRef = React.useRef<HTMLDivElement>(null);
+
+  const renderables = React.useMemo(() => {
+    return menu.flatItems;
+  }, [menu.flatItems.length]);
+  const { reverseIndexToVirtualIndex: reverseIndex } = useItemReverseIndex(renderables);
+
+  const virtualizer = useVirtualizer({
+    count: renderables.length,
+    getScrollElement: () => scrollableRef.current!,
+    estimateSize: () => 1000,
+  });
+
+  const selectedTag = useSelectedTag();
+
+  /**
+   * The side effect is responsible for moving user based on the
+   * selected tag into the API of choice in the virtualized view.
+   */
+  React.useEffect(() => {
+    const idx: number | undefined = reverseIndex[selectedTag];
+    if (!idx) {
+      return;
+    }
+
+    virtualizer.scrollToIndex(idx, {
+      align: 'start',
+    });
+  }, [selectedTag]);
+
+  return (
+    <div ref={scrollableRef} style={{ height: '100dvh', width: '100%', overflowY: 'auto' }}>
+      <ApiInfo store={store} />
+      <div
+        style={{
+          height: virtualizer.getTotalSize(),
+          width: '100%',
+          position: 'relative',
+        }}
+      >
+        {virtualizer.getVirtualItems().map(virtualItem => (
+          <div
+            key={virtualItem.key}
+            data-index={virtualItem.index}
+            ref={virtualizer.measureElement}
+            style={{
+              position: 'absolute',
+              top: 0,
+              left: 0,
+              width: '100%',
+              transform: `translateY(${virtualItem.start}px)`,
+            }}
+          >
+            <ContentItem
+              key={renderables[virtualItem.index].id}
+              item={renderables[virtualItem.index] as ContentItemModel}
+            />
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+};
+
+export default VirtualizedContent;
diff --git a/src/components/Virtualization/useItemReverseIndex.tsx b/src/components/Virtualization/useItemReverseIndex.tsx
new file mode 100644
index 0000000000..74af438081
--- /dev/null
+++ b/src/components/Virtualization/useItemReverseIndex.tsx
@@ -0,0 +1,32 @@
+import * as React from 'react';
+import { IMenuItem } from '../..';
+
+export interface MenuItemReverseIndexToVirtualIndex {
+  [key: string]: number | undefined;
+}
+
+/**
+ * Helps in calculating the Reverse Index of menu items. This will pre-compute
+ * the location in the virtualized index of each menu item IDs.
+ *
+ * The purpose is to help for faster lookup (O(1)) when user clicks the sidebar to
+ * "jump" to a certain API endpoint.
+ *
+ * @param menuItems array of IMenuItem to create the reverse index
+ * @returns key/value of id/virtualized index
+ */
+const useItemReverseIndex = (menuItems: IMenuItem[]) => {
+  const reverseIndexToVirtualIndex = React.useMemo(() => {
+    return menuItems.reduce(
+      (prev, curr, idx) => ({ ...prev, [curr.id]: idx }),
+      {} as MenuItemReverseIndexToVirtualIndex,
+    );
+
+    // It is highly unlikely an API doc to change in runtime, so we would only
+    // like to re-render if the API doc API quantity changes.
+  }, [menuItems.length]);
+
+  return { reverseIndexToVirtualIndex: reverseIndexToVirtualIndex };
+};
+
+export default useItemReverseIndex;
diff --git a/src/components/Virtualization/useSelectedTag.tsx b/src/components/Virtualization/useSelectedTag.tsx
new file mode 100644
index 0000000000..25eafcc9e3
--- /dev/null
+++ b/src/components/Virtualization/useSelectedTag.tsx
@@ -0,0 +1,40 @@
+import * as React from 'react';
+
+/**
+ * Redoc has a unique "tag" that serves as an anchor when user clicks
+ * their sidebar.
+ *
+ * For example, the tag looks like "tag/myproduct-mynamespace-myendpoint".
+ * It transforms a hash from the url into those of Redoc tag. For example,
+ * transforms "#tag/myendpoint" into "tag/myendpoint".
+ */
+export const toRedocTag = (hash: string) => {
+  return hash.substring(1, hash.length);
+};
+
+/**
+ * Helps in retrieving the redoc tag user currently activates.
+ * This is to help to redirect user into the associated API endpoint in the
+ * Virtualization Content as the traditional HTML mechanism to redirect into anchor
+ * cannot happen as not everything is rendered initially in the Virtualization Content.
+ */
+const useSelectedTag = () => {
+  const [selectedTag, setSelectedTag] = React.useState('');
+
+  React.useEffect(() => {
+    const hashCheckInterval = setInterval(() => {
+      const redocTag = toRedocTag(window.location.hash);
+      if (redocTag !== selectedTag) {
+        setSelectedTag(redocTag);
+      }
+    }, 100);
+
+    return () => {
+      clearInterval(hashCheckInterval);
+    };
+  }, [selectedTag]);
+
+  return selectedTag;
+};
+
+export default useSelectedTag;
diff --git a/src/components/__tests__/__snapshots__/FieldDetails.test.tsx.snap b/src/components/__tests__/__snapshots__/FieldDetails.test.tsx.snap
index a8b1a44fac..1e85bbb3f4 100644
--- a/src/components/__tests__/__snapshots__/FieldDetails.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/FieldDetails.test.tsx.snap
@@ -56,7 +56,7 @@ exports[`FieldDetailsComponent renders correctly 1`] = `
   </div>
   <div>
     <div
-      class="sc-lcIPJg sc-hknOHE gBHqkN jFBMaE"
+      class="sc-esYiGF sc-kAkpmW dZoiJx gPTOUI"
     >
       <p>
         test description
@@ -122,7 +122,7 @@ exports[`FieldDetailsComponent renders correctly when default value is object in
   </div>
   <div>
     <div
-      class="sc-lcIPJg sc-hknOHE gBHqkN jFBMaE"
+      class="sc-esYiGF sc-kAkpmW dZoiJx gPTOUI"
     >
       <p>
         test description
@@ -186,7 +186,7 @@ exports[`FieldDetailsComponent renders correctly when field items have string ty
   </div>
   <div>
     <div
-      class="sc-lcIPJg sc-hknOHE gBHqkN jFBMaE"
+      class="sc-esYiGF sc-kAkpmW dZoiJx gPTOUI"
     >
       <p>
         test description
diff --git a/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap b/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap
index c76566e5c3..daa2fedd47 100644
--- a/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap
@@ -1,23 +1,23 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`SecurityRequirement should render SecurityDefs 1`] = `
-"<div id="section/Authentication/petstore_auth" data-section-id="section/Authentication/petstore_auth" class="sc-dcJsrY bBkGhy"><div class="sc-kAyceB hBQWIZ"><div class="sc-fqkvVR oJKYx"><h2 class="sc-jXbUNg fWnwAh">petstore_auth</h2><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>Get access to data while protecting your account credentials.
+"<div id="section/Authentication/petstore_auth" data-section-id="section/Authentication/petstore_auth" class="sc-jXbUNg fiwqHP"><div class="sc-dAlyuH jJfjeH"><div class="sc-imWYAI euMmdx"><h2 class="sc-cwHptR ijJbCP">petstore_auth</h2><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>Get access to data while protecting your account credentials.
 OAuth2 is also a safer and more secure way to give you access.</p>
-</div><div class="sc-ejfMa-d a-DjBE"><div class="sc-dkmUuB hFwAIA"><b>Security Scheme Type: </b><span>OAuth2</span></div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Flow type: </b><code>implicit </code></div><div class="sc-dkmUuB hFwAIA"><strong> Authorization URL: </strong><code><a target="_blank" rel="noopener noreferrer" href="http://petstore.swagger.io/api/oauth/dialog">http://petstore.swagger.io/api/oauth/dialog</a></code></div><div class="sc-dkmUuB hFwAIA"><b> Scopes: </b></div><div class="sc-iEXKAA blExNw container" style="height: 4em;"><ul><li><code>write:pets</code> - <div class="sc-eeDRCY sc-eBMEME gTGgei fMmru sc-fhzFiK hXtrri redoc-markdown"><p>modify pets in your account</p>
-</div></li><li><code>read:pets</code> - <div class="sc-eeDRCY sc-eBMEME gTGgei fMmru sc-fhzFiK hXtrri redoc-markdown"><p>read your pets</p>
-</div></li></ul></div><div class="sc-EgOXT bNSpXO"></div></div></div></div></div></div><div id="section/Authentication/GitLab_PersonalAccessToken" data-section-id="section/Authentication/GitLab_PersonalAccessToken" class="sc-dcJsrY bBkGhy"><div class="sc-kAyceB hBQWIZ"><div class="sc-fqkvVR oJKYx"><h2 class="sc-jXbUNg fWnwAh">GitLab_PersonalAccessToken</h2><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>GitLab Personal Access Token description</p>
-</div><div class="sc-ejfMa-d a-DjBE"><div class="sc-dkmUuB hFwAIA"><b>Security Scheme Type: </b><span>API Key</span></div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Header parameter name: </b><code>PRIVATE-TOKEN</code></div></div></div></div></div></div><div id="section/Authentication/GitLab_OpenIdConnect" data-section-id="section/Authentication/GitLab_OpenIdConnect" class="sc-dcJsrY bBkGhy"><div class="sc-kAyceB hBQWIZ"><div class="sc-fqkvVR oJKYx"><h2 class="sc-jXbUNg fWnwAh">GitLab_OpenIdConnect</h2><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>GitLab OpenIdConnect description</p>
-</div><div class="sc-ejfMa-d a-DjBE"><div class="sc-dkmUuB hFwAIA"><b>Security Scheme Type: </b><span>OpenID Connect</span></div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Connect URL: </b><code><a target="_blank" rel="noopener noreferrer" href="https://gitlab.com/.well-known/openid-configuration">https://gitlab.com/.well-known/openid-configuration</a></code></div></div></div></div></div></div><div id="section/Authentication/basicAuth" data-section-id="section/Authentication/basicAuth" class="sc-dcJsrY bBkGhy"><div class="sc-kAyceB hBQWIZ"><div class="sc-fqkvVR oJKYx"><h2 class="sc-jXbUNg fWnwAh">basicAuth</h2><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"></div><div class="sc-ejfMa-d a-DjBE"><div class="sc-dkmUuB hFwAIA"><b>Security Scheme Type: </b><span>HTTP</span></div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>HTTP Authorization Scheme: </b><code>basic</code></div><div class="sc-dkmUuB hFwAIA"></div></div></div></div></div></div>"
+</div><div class="sc-bVHCgj jeamQm"><div class="sc-bDpDS hgsvLV"><b>Security Scheme Type: </b><span>OAuth2</span></div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Flow type: </b><code>implicit </code></div><div class="sc-bDpDS hgsvLV"><strong> Authorization URL: </strong><code><a target="_blank" rel="noopener noreferrer" href="http://petstore.swagger.io/api/oauth/dialog">http://petstore.swagger.io/api/oauth/dialog</a></code></div><div class="sc-bDpDS hgsvLV"><b> Scopes: </b></div><div class="sc-dSIIpw iEDmXP container" style="height: 4em;"><ul><li><code>write:pets</code> - <div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK sc-klVQfs cfshkX redoc-markdown"><p>modify pets in your account</p>
+</div></li><li><code>read:pets</code> - <div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK sc-klVQfs cfshkX redoc-markdown"><p>read your pets</p>
+</div></li></ul></div><div class="sc-fMMURN eKpvEA"></div></div></div></div></div></div><div id="section/Authentication/GitLab_PersonalAccessToken" data-section-id="section/Authentication/GitLab_PersonalAccessToken" class="sc-jXbUNg fiwqHP"><div class="sc-dAlyuH jJfjeH"><div class="sc-imWYAI euMmdx"><h2 class="sc-cwHptR ijJbCP">GitLab_PersonalAccessToken</h2><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>GitLab Personal Access Token description</p>
+</div><div class="sc-bVHCgj jeamQm"><div class="sc-bDpDS hgsvLV"><b>Security Scheme Type: </b><span>API Key</span></div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Header parameter name: </b><code>PRIVATE-TOKEN</code></div></div></div></div></div></div><div id="section/Authentication/GitLab_OpenIdConnect" data-section-id="section/Authentication/GitLab_OpenIdConnect" class="sc-jXbUNg fiwqHP"><div class="sc-dAlyuH jJfjeH"><div class="sc-imWYAI euMmdx"><h2 class="sc-cwHptR ijJbCP">GitLab_OpenIdConnect</h2><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>GitLab OpenIdConnect description</p>
+</div><div class="sc-bVHCgj jeamQm"><div class="sc-bDpDS hgsvLV"><b>Security Scheme Type: </b><span>OpenID Connect</span></div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Connect URL: </b><code><a target="_blank" rel="noopener noreferrer" href="https://gitlab.com/.well-known/openid-configuration">https://gitlab.com/.well-known/openid-configuration</a></code></div></div></div></div></div></div><div id="section/Authentication/basicAuth" data-section-id="section/Authentication/basicAuth" class="sc-jXbUNg fiwqHP"><div class="sc-dAlyuH jJfjeH"><div class="sc-imWYAI euMmdx"><h2 class="sc-cwHptR ijJbCP">basicAuth</h2><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"></div><div class="sc-bVHCgj jeamQm"><div class="sc-bDpDS hgsvLV"><b>Security Scheme Type: </b><span>HTTP</span></div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>HTTP Authorization Scheme: </b><code>basic</code></div><div class="sc-bDpDS hgsvLV"></div></div></div></div></div></div>"
 `;
 
-exports[`SecurityRequirement should render authDefinition 1`] = `"<div class="sc-bDumWk iWBBny"><div class="sc-sLsrZ hgeUJn"><h5 class="sc-dAlyuH sc-fifgRP jbQuod kWJur">Authorizations:</h5><svg class="sc-cwHptR iZRiKW" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-dBmzty eoFcYg"><span class="sc-kbousE cpXQuZ">(<span class="sc-gfoqjT kbvnry">API Key: <i>GitLab_PersonalAccessToken</i></span><span class="sc-gfoqjT kbvnry">OpenID Connect: <i>GitLab_OpenIdConnect</i></span><span class="sc-gfoqjT kbvnry">HTTP: <i>basicAuth</i></span>) </span><span class="sc-kbousE cpXQuZ"><span class="sc-gfoqjT kbvnry">OAuth2: <i>petstore_auth</i></span></span></div></div>,"`;
+exports[`SecurityRequirement should render authDefinition 1`] = `"<div class="sc-kzqdkY iBGSSi"><div class="sc-kWtpeL btBUKm"><h5 class="sc-dLMFU sc-gdyeKB lcezuG gSXcBp">Authorizations:</h5><svg class="sc-gsFSXq cOXSQA" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-ecPEgm cOTqGl"><span class="sc-hHOBiw jggLOL">(<span class="sc-dlWCHZ dCGAkl">API Key: <i>GitLab_PersonalAccessToken</i></span><span class="sc-dlWCHZ dCGAkl">OpenID Connect: <i>GitLab_OpenIdConnect</i></span><span class="sc-dlWCHZ dCGAkl">HTTP: <i>basicAuth</i></span>) </span><span class="sc-hHOBiw jggLOL"><span class="sc-dlWCHZ dCGAkl">OAuth2: <i>petstore_auth</i></span></span></div></div>,"`;
 
 exports[`SecurityRequirement should render authDefinition 2`] = `
-"<div class="sc-bDumWk gtsPcy"><div class="sc-sLsrZ hgeUJn"><h5 class="sc-dAlyuH sc-fifgRP jbQuod kWJur">Authorizations:</h5><svg class="sc-cwHptR dSJqIk" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-dBmzty llvZdI"><span class="sc-kbousE dOwJQz">(<span class="sc-gfoqjT kbvnry">API Key: <i>GitLab_PersonalAccessToken</i></span><span class="sc-gfoqjT kbvnry">OpenID Connect: <i>GitLab_OpenIdConnect</i></span><span class="sc-gfoqjT kbvnry">HTTP: <i>basicAuth</i></span>) </span><span class="sc-kbousE dOwJQz"><span class="sc-gfoqjT kbvnry">OAuth2: <i>petstore_auth</i> (<code class="sc-eyvILC bzHwfc">write:pets</code><code class="sc-eyvILC bzHwfc">read:pets</code>) </span></span></div></div><div class="sc-ejfMa-d a-DjBE"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> OAuth2: petstore_auth</h5><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>Get access to data while protecting your account credentials.
+"<div class="sc-kzqdkY ftTbsm"><div class="sc-kWtpeL btBUKm"><h5 class="sc-dLMFU sc-gdyeKB lcezuG gSXcBp">Authorizations:</h5><svg class="sc-gsFSXq eWnmLm" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-ecPEgm fYRInh"><span class="sc-hHOBiw bWBltB">(<span class="sc-dlWCHZ dCGAkl">API Key: <i>GitLab_PersonalAccessToken</i></span><span class="sc-dlWCHZ dCGAkl">OpenID Connect: <i>GitLab_OpenIdConnect</i></span><span class="sc-dlWCHZ dCGAkl">HTTP: <i>basicAuth</i></span>) </span><span class="sc-hHOBiw bWBltB"><span class="sc-dlWCHZ dCGAkl">OAuth2: <i>petstore_auth</i> (<code class="sc-eZYNyq eZCLxU">write:pets</code><code class="sc-eZYNyq eZCLxU">read:pets</code>) </span></span></div></div><div class="sc-bVHCgj jeamQm"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> OAuth2: petstore_auth</h5><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>Get access to data while protecting your account credentials.
 OAuth2 is also a safer and more secure way to give you access.</p>
-</div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Flow type: </b><code>implicit </code></div><div class="sc-dkmUuB hFwAIA"><strong> Authorization URL: </strong><code><a target="_blank" rel="noopener noreferrer" href="http://petstore.swagger.io/api/oauth/dialog">http://petstore.swagger.io/api/oauth/dialog</a></code></div><div><b>Required scopes: </b><code>write:pets</code> <code>read:pets</code> </div><div class="sc-dkmUuB hFwAIA"><b> Scopes: </b></div><div class="sc-iEXKAA blExNw container" style="height: 4em;"><ul><li><code>write:pets</code> - <div class="sc-eeDRCY sc-eBMEME gTGgei fMmru sc-fhzFiK hXtrri redoc-markdown"><p>modify pets in your account</p>
-</div></li><li><code>read:pets</code> - <div class="sc-eeDRCY sc-eBMEME gTGgei fMmru sc-fhzFiK hXtrri redoc-markdown"><p>read your pets</p>
-</div></li></ul></div><div class="sc-EgOXT bNSpXO"></div></div></div><div class="sc-ejfMa-d a-DjBE"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> API Key: GitLab_PersonalAccessToken</h5><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>GitLab Personal Access Token description</p>
-</div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Header parameter name: </b><code>PRIVATE-TOKEN</code></div></div></div><div class="sc-ejfMa-d a-DjBE"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> OpenID Connect: GitLab_OpenIdConnect</h5><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><p>GitLab OpenIdConnect description</p>
-</div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>Connect URL: </b><code><a target="_blank" rel="noopener noreferrer" href="https://gitlab.com/.well-known/openid-configuration">https://gitlab.com/.well-known/openid-configuration</a></code></div></div></div><div class="sc-ejfMa-d a-DjBE"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> HTTP: basicAuth</h5><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"></div><div class="sc-eeDRCY sc-eBMEME gTGgei fMmru"><div class="sc-dkmUuB hFwAIA"><b>HTTP Authorization Scheme: </b><code>basic</code></div><div class="sc-dkmUuB hFwAIA"></div></div></div>,"
+</div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Flow type: </b><code>implicit </code></div><div class="sc-bDpDS hgsvLV"><strong> Authorization URL: </strong><code><a target="_blank" rel="noopener noreferrer" href="http://petstore.swagger.io/api/oauth/dialog">http://petstore.swagger.io/api/oauth/dialog</a></code></div><div><b>Required scopes: </b><code>write:pets</code> <code>read:pets</code> </div><div class="sc-bDpDS hgsvLV"><b> Scopes: </b></div><div class="sc-dSIIpw iEDmXP container" style="height: 4em;"><ul><li><code>write:pets</code> - <div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK sc-klVQfs cfshkX redoc-markdown"><p>modify pets in your account</p>
+</div></li><li><code>read:pets</code> - <div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK sc-klVQfs cfshkX redoc-markdown"><p>read your pets</p>
+</div></li></ul></div><div class="sc-fMMURN eKpvEA"></div></div></div><div class="sc-bVHCgj jeamQm"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> API Key: GitLab_PersonalAccessToken</h5><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>GitLab Personal Access Token description</p>
+</div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Header parameter name: </b><code>PRIVATE-TOKEN</code></div></div></div><div class="sc-bVHCgj jeamQm"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> OpenID Connect: GitLab_OpenIdConnect</h5><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><p>GitLab OpenIdConnect description</p>
+</div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>Connect URL: </b><code><a target="_blank" rel="noopener noreferrer" href="https://gitlab.com/.well-known/openid-configuration">https://gitlab.com/.well-known/openid-configuration</a></code></div></div></div><div class="sc-bVHCgj jeamQm"><h5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M18 10V6A6 6 0 0 0 6 6v4H3v14h18V10h-3zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v4H8V6zm11 16H5V12h14v10z"></path></svg> HTTP: basicAuth</h5><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"></div><div class="sc-iHGNWf sc-hRJfrW kIgucL kKrqUK"><div class="sc-bDpDS hgsvLV"><b>HTTP Authorization Scheme: </b><code>basic</code></div><div class="sc-bDpDS hgsvLV"></div></div></div>,"
 `;
diff --git a/src/components/__tests__/useItemReverseIndex.test.tsx b/src/components/__tests__/useItemReverseIndex.test.tsx
new file mode 100644
index 0000000000..b15ad2b4d1
--- /dev/null
+++ b/src/components/__tests__/useItemReverseIndex.test.tsx
@@ -0,0 +1,100 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom/client';
+import { IMenuItem } from '../..';
+import useItemReverseIndex, {
+  MenuItemReverseIndexToVirtualIndex,
+} from '../Virtualization/useItemReverseIndex';
+
+(global as any).IS_REACT_ACT_ENVIRONMENT = true;
+
+describe('useItemReverseIndex', () => {
+  let container: HTMLDivElement;
+  let root: ReactDOM.Root;
+
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = ReactDOM.createRoot(container);
+  });
+
+  afterEach(() => {
+    React.act(() => root.unmount());
+    document.body.removeChild(container);
+    container = null as any;
+  });
+
+  it('it should maps item based on the id for quick lookup', () => {
+    const menuItems: any[] = [
+      { id: 'item1', name: 'Item 1' },
+      { id: 'item2', name: 'Item 2' },
+      { id: 'item3', name: 'Item 3' },
+    ];
+
+    let result: MenuItemReverseIndexToVirtualIndex | undefined;
+
+    // Hooks can only be used inside a React component.
+    // This component is just to host the hook.
+    const TestComponent = ({ items }: { items: IMenuItem[] }) => {
+      const { reverseIndexToVirtualIndex } = useItemReverseIndex(items);
+      result = reverseIndexToVirtualIndex; // this is to capture the hook's output for later's assertions
+      return null;
+    };
+
+    // Render component within act to handle React state updates
+    React.act(() => {
+      root.render(<TestComponent items={menuItems} />);
+    });
+
+    const expectedMapping: MenuItemReverseIndexToVirtualIndex = {
+      item1: 0,
+      item2: 1,
+      item3: 2,
+    };
+
+    expect(result).toEqual(expectedMapping);
+  });
+
+  // Note: the test below only tests when the items change in quantity.
+  // This is because it is very unlikely for an API docs to change in the first-place,
+  // so just-in-case, I only allow it to re-render when the items change in quantity.
+  it('should update the mapping when menu items change in quantity', () => {
+    let result: MenuItemReverseIndexToVirtualIndex | undefined;
+
+    const initialItems: any[] = [
+      { id: 'item1', description: 'Item 1' },
+      { id: 'item2', description: 'Item 2' },
+    ];
+
+    const newItems: any[] = [
+      { id: 'newItem1', description: 'New Item 1' },
+      { id: 'newItem2', description: 'New Item 2' },
+      { id: 'newItem3', description: 'New Item 3' },
+    ];
+
+    // Hooks can only be used inside a React component.
+    // This component is just to host the hook.
+    const TestComponent = ({ items }: { items: IMenuItem[] }) => {
+      const { reverseIndexToVirtualIndex } = useItemReverseIndex(items);
+      result = reverseIndexToVirtualIndex;
+      return null;
+    };
+
+    // Initial render
+    React.act(() => {
+      root.render(<TestComponent items={initialItems} />);
+    });
+
+    // Update render with new items
+    React.act(() => {
+      root.render(<TestComponent items={newItems} />);
+    });
+
+    const expectedNewMapping: MenuItemReverseIndexToVirtualIndex = {
+      newItem1: 0,
+      newItem2: 1,
+      newItem3: 2,
+    };
+
+    expect(result).toEqual(expectedNewMapping);
+  });
+});
diff --git a/src/components/__tests__/useSelectedTag.test.tsx b/src/components/__tests__/useSelectedTag.test.tsx
new file mode 100644
index 0000000000..e4c47f965f
--- /dev/null
+++ b/src/components/__tests__/useSelectedTag.test.tsx
@@ -0,0 +1,134 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom/client';
+import useSelectedTag from '../Virtualization/useSelectedTag';
+
+(global as any).IS_REACT_ACT_ENVIRONMENT = true;
+
+jest.useFakeTimers();
+
+describe('useSelectedTag', () => {
+  let container: HTMLDivElement;
+  let root: ReactDOM.Root;
+
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    root = ReactDOM.createRoot(container);
+  });
+
+  afterEach(() => {
+    React.act(() => root.unmount());
+    document.body.removeChild(container);
+    container = null as any;
+    jest.clearAllTimers();
+  });
+
+  it('should return the correct tag based on the hash in the URL', async () => {
+    window.location.hash = '#tag/product-namespace-verb';
+
+    let selectedTag: string | undefined;
+
+    // Hooks can only be used inside a React component.
+    // This component is just to host the hook.
+    const TestComponent = () => {
+      selectedTag = useSelectedTag();
+      return <p data-testid={selectedTag}>test</p>;
+    };
+
+    React.act(() => {
+      root.render(<TestComponent />);
+    });
+
+    await React.act(async () => {
+      jest.advanceTimersByTime(100);
+    });
+
+    expect(selectedTag).toBe('tag/product-namespace-verb');
+  });
+
+  it('resetting a tag will also reset the selected tag from the hook', async () => {
+    let selectedTag: string | undefined;
+
+    // Hooks can only be used inside a React component.
+    // This component is just to host the hook.
+    const TestComponent = () => {
+      selectedTag = useSelectedTag();
+      return <p data-testid={selectedTag}>test</p>;
+    };
+
+    React.act(() => {
+      root.render(<TestComponent />);
+    });
+
+    expect(selectedTag).toBe('');
+
+    window.location.hash = '#tag/product-namespace-verb';
+    await React.act(async () => {
+      jest.advanceTimersByTime(100);
+    });
+    expect(selectedTag).toBe('tag/product-namespace-verb');
+
+    window.location.hash = '';
+    await React.act(async () => {
+      jest.advanceTimersByTime(100);
+    });
+    expect(selectedTag).toBe('');
+  });
+
+  it('should update the selected tag when hash changes in the URL', async () => {
+    window.location.hash = '#tag/product-namespace-verb';
+
+    let selectedTag: string | undefined;
+
+    // Hooks can only be used inside a React component.
+    // This component is just to host the hook.
+    const TestComponent = () => {
+      selectedTag = useSelectedTag();
+      return null;
+    };
+
+    React.act(() => {
+      root.render(<TestComponent />);
+    });
+
+    await React.act(async () => {
+      jest.advanceTimersByTime(100);
+    });
+    expect(selectedTag).toBe('tag/product-namespace-verb');
+
+    window.location.hash =
+      '#tag/product-namespace-verb/operation/product-namespace-verb_OperationID';
+    await React.act(async () => {
+      jest.advanceTimersByTime(100);
+    });
+    expect(selectedTag).toBe(
+      'tag/product-namespace-verb/operation/product-namespace-verb_OperationID',
+    );
+  });
+
+  it('should clear the interval on component unmount', () => {
+    const clearIntervalSpy = jest.spyOn(window, 'clearInterval');
+
+    // Hooks can only be used inside a React component.
+    // This component is just to host the hook.
+    const TestComponent = () => {
+      useSelectedTag();
+      return null;
+    };
+
+    React.act(() => {
+      root.render(<TestComponent />);
+    });
+
+    // Ensure the component is mounted
+    expect(clearIntervalSpy).not.toHaveBeenCalled();
+
+    // Unmount the component
+    React.act(() => {
+      root.unmount();
+    });
+
+    expect(clearIntervalSpy).toHaveBeenCalledTimes(1);
+    clearIntervalSpy.mockRestore();
+  });
+});

From 80509a8cebdac9f266d76a250db0d4af423f808c Mon Sep 17 00:00:00 2001
From: Putu Audi Pasuatmadi <63685606+audipasuatmadi@users.noreply.github.com>
Date: Tue, 26 Nov 2024 13:59:10 +0800
Subject: [PATCH 2/4] feat: make virtualization configurable where the default
 is not using virtualization

---
 docs/config.md                                        |  6 ++++++
 src/components/Redoc/Redoc.tsx                        | 11 ++++++++++-
 .../__snapshots__/DiscriminatorDropdown.test.tsx.snap | 10 ++++++++++
 src/services/RedocNormalizedOptions.ts                |  5 +++++
 4 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/docs/config.md b/docs/config.md
index 28e24f7eda..1648e95824 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -166,6 +166,12 @@ _Default: false_
 
 If set to `true`, the API definition is considered untrusted and all HTML/Markdown is sanitized to prevent XSS.
 
+### enableVirtualization
+
+If set to `true`, the API documentation content will use virtualization. Virtualization only renders the API content when it is currently visible in user's viewport.
+
+_Default: false_
+
 ## Theme settings
 
 * `spacing`
diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx
index bc12a84d18..483860fa77 100644
--- a/src/components/Redoc/Redoc.tsx
+++ b/src/components/Redoc/Redoc.tsx
@@ -14,6 +14,8 @@ import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements';
 import { SearchBox } from '../SearchBox/SearchBox';
 import { StoreProvider } from '../StoreBuilder';
 import VirtualizedContent from '../Virtualization/VirtualizedContent';
+import { ApiInfo } from '../ApiInfo/ApiInfo';
+import { ContentItems } from '../ContentItems/ContentItems';
 
 export interface RedocProps {
   store: AppStore;
@@ -56,7 +58,14 @@ export class Redoc extends React.Component<RedocProps> {
                 <SideMenu menu={menu} />
               </StickyResponsiveSidebar>
               <ApiContentWrap className="api-content">
-                <VirtualizedContent store={store} menu={menu} />
+                {options.enableVirtualization ? (
+                  <VirtualizedContent store={store} menu={menu} />
+                ) : (
+                  <>
+                    <ApiInfo store={store} />
+                    <ContentItems items={menu.items as any} />
+                  </>
+                )}
               </ApiContentWrap>
               <BackgroundStub />
             </RedocWrap>
diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap
index 488199829b..49d0986ef2 100644
--- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap
@@ -79,6 +79,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
                     "disableSearch": false,
                     "downloadDefinitionUrl": undefined,
                     "downloadFileName": undefined,
+                    "enableVirtualization": false,
                     "enumSkipQuotes": false,
                     "expandDefaultServerVariables": false,
                     "expandResponses": {},
@@ -351,6 +352,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
                     "disableSearch": false,
                     "downloadDefinitionUrl": undefined,
                     "downloadFileName": undefined,
+                    "enableVirtualization": false,
                     "enumSkipQuotes": false,
                     "expandDefaultServerVariables": false,
                     "expandResponses": {},
@@ -610,6 +612,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
               "disableSearch": false,
               "downloadDefinitionUrl": undefined,
               "downloadFileName": undefined,
+              "enableVirtualization": false,
               "enumSkipQuotes": false,
               "expandDefaultServerVariables": false,
               "expandResponses": {},
@@ -931,6 +934,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
                     "disableSearch": false,
                     "downloadDefinitionUrl": undefined,
                     "downloadFileName": undefined,
+                    "enableVirtualization": false,
                     "enumSkipQuotes": false,
                     "expandDefaultServerVariables": false,
                     "expandResponses": {},
@@ -1215,6 +1219,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
                     "disableSearch": false,
                     "downloadDefinitionUrl": undefined,
                     "downloadFileName": undefined,
+                    "enableVirtualization": false,
                     "enumSkipQuotes": false,
                     "expandDefaultServerVariables": false,
                     "expandResponses": {},
@@ -1470,6 +1475,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
               "disableSearch": false,
               "downloadDefinitionUrl": undefined,
               "downloadFileName": undefined,
+              "enableVirtualization": false,
               "enumSkipQuotes": false,
               "expandDefaultServerVariables": false,
               "expandResponses": {},
@@ -1750,6 +1756,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
           "disableSearch": false,
           "downloadDefinitionUrl": undefined,
           "downloadFileName": undefined,
+          "enableVirtualization": false,
           "enumSkipQuotes": false,
           "expandDefaultServerVariables": false,
           "expandResponses": {},
@@ -2060,6 +2067,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
               "disableSearch": false,
               "downloadDefinitionUrl": undefined,
               "downloadFileName": undefined,
+              "enableVirtualization": false,
               "enumSkipQuotes": false,
               "expandDefaultServerVariables": false,
               "expandResponses": {},
@@ -2332,6 +2340,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
               "disableSearch": false,
               "downloadDefinitionUrl": undefined,
               "downloadFileName": undefined,
+              "enableVirtualization": false,
               "enumSkipQuotes": false,
               "expandDefaultServerVariables": false,
               "expandResponses": {},
@@ -2591,6 +2600,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
         "disableSearch": false,
         "downloadDefinitionUrl": undefined,
         "downloadFileName": undefined,
+        "enableVirtualization": false,
         "enumSkipQuotes": false,
         "expandDefaultServerVariables": false,
         "expandResponses": {},
diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts
index 0cdd7f9e2a..bdacc0878b 100644
--- a/src/services/RedocNormalizedOptions.ts
+++ b/src/services/RedocNormalizedOptions.ts
@@ -57,6 +57,8 @@ export interface RedocRawOptions {
   hideFab?: boolean;
   minCharacterLengthToInitSearch?: number;
   showWebhookVerb?: boolean;
+
+  enableVirtualization?: boolean | string;
 }
 
 export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
@@ -259,6 +261,8 @@ export class RedocNormalizedOptions {
   minCharacterLengthToInitSearch: number;
   showWebhookVerb: boolean;
 
+  enableVirtualization: boolean;
+
   nonce?: string;
 
   constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
@@ -338,5 +342,6 @@ export class RedocNormalizedOptions {
     this.hideFab = argValueToBoolean(raw.hideFab);
     this.minCharacterLengthToInitSearch = argValueToNumber(raw.minCharacterLengthToInitSearch) || 3;
     this.showWebhookVerb = argValueToBoolean(raw.showWebhookVerb);
+    this.enableVirtualization = argValueToBoolean(raw.enableVirtualization);
   }
 }

From 894224544ce2fdad9324c7ddcf4c0cdc53b55e10 Mon Sep 17 00:00:00 2001
From: Putu Audi Pasuatmadi <audipasuatmadi@users.noreply.github.com>
Date: Tue, 26 Nov 2024 14:19:18 +0800
Subject: [PATCH 3/4] fix: decode uri component from hash to match the id and
 data-section-id of the api endpoint

---
 .../Virtualization/useSelectedTag.tsx         |  4 +++-
 .../__tests__/useSelectedTag.test.tsx         | 23 +++++++++++++++++++
 2 files changed, 26 insertions(+), 1 deletion(-)

diff --git a/src/components/Virtualization/useSelectedTag.tsx b/src/components/Virtualization/useSelectedTag.tsx
index 25eafcc9e3..140252fba8 100644
--- a/src/components/Virtualization/useSelectedTag.tsx
+++ b/src/components/Virtualization/useSelectedTag.tsx
@@ -9,7 +9,8 @@ import * as React from 'react';
  * transforms "#tag/myendpoint" into "tag/myendpoint".
  */
 export const toRedocTag = (hash: string) => {
-  return hash.substring(1, hash.length);
+  const decodedHash = decodeURIComponent(hash);
+  return decodedHash.substring(1, decodedHash.length);
 };
 
 /**
@@ -24,6 +25,7 @@ const useSelectedTag = () => {
   React.useEffect(() => {
     const hashCheckInterval = setInterval(() => {
       const redocTag = toRedocTag(window.location.hash);
+      console.log(redocTag);
       if (redocTag !== selectedTag) {
         setSelectedTag(redocTag);
       }
diff --git a/src/components/__tests__/useSelectedTag.test.tsx b/src/components/__tests__/useSelectedTag.test.tsx
index e4c47f965f..6548aabc4f 100644
--- a/src/components/__tests__/useSelectedTag.test.tsx
+++ b/src/components/__tests__/useSelectedTag.test.tsx
@@ -46,6 +46,29 @@ describe('useSelectedTag', () => {
     expect(selectedTag).toBe('tag/product-namespace-verb');
   });
 
+  it('uri encoded tags are decoded to match the tags in the actual html id and data-section-id', async () => {
+    window.location.hash = '#tag/Notes/paths/~1notes~1%7Bid%7D/get';
+
+    let selectedTag: string | undefined;
+
+    // Hooks can only be used inside a React component.
+    // This component is just to host the hook.
+    const TestComponent = () => {
+      selectedTag = useSelectedTag();
+      return <p data-testid={selectedTag}>test</p>;
+    };
+
+    React.act(() => {
+      root.render(<TestComponent />);
+    });
+
+    await React.act(async () => {
+      jest.advanceTimersByTime(100);
+    });
+
+    expect(selectedTag).toBe('tag/Notes/paths/~1notes~1{id}/get');
+  });
+
   it('resetting a tag will also reset the selected tag from the hook', async () => {
     let selectedTag: string | undefined;
 

From 43e1b3df0c38143057a7680170874a5d94dd7fd8 Mon Sep 17 00:00:00 2001
From: Putu Audi Pasuatmadi <audipasuatmadi@users.noreply.github.com>
Date: Tue, 26 Nov 2024 14:41:56 +0800
Subject: [PATCH 4/4] fix: remove magic numbers

---
 src/components/Virtualization/VirtualizedContent.tsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/components/Virtualization/VirtualizedContent.tsx b/src/components/Virtualization/VirtualizedContent.tsx
index bdf8f86f05..ed9a2a965e 100644
--- a/src/components/Virtualization/VirtualizedContent.tsx
+++ b/src/components/Virtualization/VirtualizedContent.tsx
@@ -9,6 +9,8 @@ type VirtualizedContentProps = {
   menu: MenuStore;
 };
 
+const ESTIMATED_EACH_API_HEIGHT_PX = 1000;
+
 /**
  * VirtualizedContent optimizes the rendering of API documentation in Redoc by virtualizing the content.
  *
@@ -21,7 +23,6 @@ type VirtualizedContentProps = {
  * by reducing the amount of content loaded into memory at any one time, thereby enhancing
  * performance and preventing potential crashes due to excessive memory usage.
  *
- * @author Audi
  */
 const VirtualizedContent = ({ store, menu }: VirtualizedContentProps) => {
   const scrollableRef = React.useRef<HTMLDivElement>(null);
@@ -34,7 +35,7 @@ const VirtualizedContent = ({ store, menu }: VirtualizedContentProps) => {
   const virtualizer = useVirtualizer({
     count: renderables.length,
     getScrollElement: () => scrollableRef.current!,
-    estimateSize: () => 1000,
+    estimateSize: () => ESTIMATED_EACH_API_HEIGHT_PX,
   });
 
   const selectedTag = useSelectedTag();