diff --git a/.changeset/good-files-enjoy.md b/.changeset/good-files-enjoy.md
new file mode 100644
index 00000000000..f8adb1b7923
--- /dev/null
+++ b/.changeset/good-files-enjoy.md
@@ -0,0 +1,5 @@
+---
+'@graphiql/plugin-doc-explorer': minor
+---
+
+Virtualize the All Types list in the Docs pane when the schema contains more than 1000 elements
diff --git a/packages/graphiql-plugin-doc-explorer/package.json b/packages/graphiql-plugin-doc-explorer/package.json
index 3cb1ccb77f2..718645fc578 100644
--- a/packages/graphiql-plugin-doc-explorer/package.json
+++ b/packages/graphiql-plugin-doc-explorer/package.json
@@ -45,6 +45,7 @@
},
"dependencies": {
"@headlessui/react": "^2.2",
+ "@tanstack/react-virtual": "^3.13.24",
"react-compiler-runtime": "19.1.0-rc.1",
"zustand": "^5"
},
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/schema-documentation.spec.tsx b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/schema-documentation.spec.tsx
new file mode 100644
index 00000000000..b3dcb358dfb
--- /dev/null
+++ b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/schema-documentation.spec.tsx
@@ -0,0 +1,62 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { render } from '@testing-library/react';
+import {
+ GraphQLObjectType,
+ GraphQLSchema,
+ GraphQLString,
+ type GraphQLFieldConfigMap,
+} from 'graphql';
+import { SchemaDocumentation } from '../schema-documentation';
+import { VirtualList } from '../virtual-list';
+
+vi.mock('../virtual-list', () => ({
+ VirtualList: vi.fn(() =>
),
+}));
+
+const VirtualListMock = vi.mocked(VirtualList);
+
+function makeSchemaWithNTypes(typeCount: number): GraphQLSchema {
+ const userTypes = Array.from(
+ { length: typeCount },
+ (_, i) =>
+ new GraphQLObjectType({
+ name: `UserType${i}`,
+ fields: { name: { type: GraphQLString } },
+ }),
+ );
+ const queryFields: GraphQLFieldConfigMap = {};
+ for (const [i, t] of userTypes.entries()) {
+ queryFields[`field${i}`] = { type: t };
+ }
+ return new GraphQLSchema({
+ query: new GraphQLObjectType({ name: 'Query', fields: queryFields }),
+ types: userTypes,
+ });
+}
+
+describe('SchemaDocumentation', () => {
+ beforeEach(() => {
+ VirtualListMock.mockClear();
+ });
+
+ it('renders VirtualList when there are more than 1000 types in allTypes', () => {
+ const schema = makeSchemaWithNTypes(1500);
+ render();
+
+ expect(VirtualListMock).toHaveBeenCalledTimes(1);
+ const items = VirtualListMock.mock.calls[0]![0].items as unknown[];
+ expect(items.length).toBeGreaterThan(1000);
+ });
+
+ it('renders a plain list (not VirtualList) for small schemas', () => {
+ const schema = makeSchemaWithNTypes(5);
+ const { container } = render();
+
+ expect(VirtualListMock).not.toHaveBeenCalled();
+ // Every UserType plus the built-in String scalar should render a TypeLink.
+ const typeLinks = container.querySelectorAll(
+ '.graphiql-doc-explorer-section--all-types .graphiql-doc-explorer-type-name',
+ );
+ expect(typeLinks.length).toBeGreaterThanOrEqual(5);
+ });
+});
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/virtual-list.spec.tsx b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/virtual-list.spec.tsx
new file mode 100644
index 00000000000..ca53f08d7fe
--- /dev/null
+++ b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/virtual-list.spec.tsx
@@ -0,0 +1,38 @@
+import { describe, expect, it } from 'vitest';
+import { render } from '@testing-library/react';
+import { VirtualList } from '../virtual-list';
+
+describe('VirtualList', () => {
+ it('renders every item via the fallback path when the scroll element has no layout (JSDOM)', () => {
+ const items = Array.from({ length: 5 }, (_, i) => `item-${i}`);
+ const { container } = render(
+ 36}
+ renderItem={item => {item}}
+ />,
+ );
+
+ const rendered = container.querySelectorAll('[data-testid="item"]');
+ expect(rendered).toHaveLength(5);
+ expect(Array.from(rendered, el => el.textContent)).toEqual([
+ 'item-0',
+ 'item-1',
+ 'item-2',
+ 'item-3',
+ 'item-4',
+ ]);
+ });
+
+ it('renders nothing and does not crash with an empty items array', () => {
+ const { container } = render(
+ 36}
+ renderItem={item => {String(item)}}
+ />,
+ );
+
+ expect(container.querySelectorAll('[data-testid="item"]')).toHaveLength(0);
+ });
+});
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.css b/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.css
index 88178fa122d..31e69362228 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.css
+++ b/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.css
@@ -1,3 +1,9 @@
+.graphiql-doc-explorer {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
/* The header of the doc explorer */
.graphiql-doc-explorer-header {
display: flex;
@@ -31,6 +37,7 @@
position: absolute;
right: 0;
top: 0;
+ z-index: 2;
&:focus-within {
left: 0;
@@ -87,6 +94,14 @@ a.graphiql-doc-explorer-back {
}
/* The contents of the currently active page in the doc explorer */
+.graphiql-doc-explorer-content {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ min-height: 0;
+ overflow: auto;
+}
+
.graphiql-doc-explorer-content > * {
color: hsla(var(--color-neutral), var(--alpha-secondary));
margin-top: var(--px-20);
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/schema-documentation.tsx b/packages/graphiql-plugin-doc-explorer/src/components/schema-documentation.tsx
index 280533644c7..343d087c489 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/schema-documentation.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/schema-documentation.tsx
@@ -3,6 +3,7 @@ import type { GraphQLSchema } from 'graphql';
import { MarkdownContent } from '@graphiql/react';
import { ExplorerSection } from './section';
import { TypeLink } from './type-link';
+import { VirtualList } from './virtual-list';
import './schema-documentation.css';
type SchemaDocumentationProps = {
@@ -12,6 +13,8 @@ type SchemaDocumentationProps = {
schema: GraphQLSchema;
};
+const UNVIRTUALIZED_MAX_LENGTH = 1000;
+
export const SchemaDocumentation: FC = ({
schema,
}) => {
@@ -24,6 +27,11 @@ export const SchemaDocumentation: FC = ({
mutationType?.name,
subscriptionType?.name,
];
+ const allTypes = Object.values(typeMap).filter(
+ type =>
+ !ignoreTypesInAllSchema.includes(type.name) &&
+ !type.name.startsWith('__'),
+ );
return (
<>
@@ -56,23 +64,22 @@ export const SchemaDocumentation: FC = ({
)}
-
-
- {Object.values(typeMap).map(type => {
- if (
- ignoreTypesInAllSchema.includes(type.name) ||
- type.name.startsWith('__')
- ) {
- return null;
- }
-
- return (
+
+ {allTypes.length > UNVIRTUALIZED_MAX_LENGTH ? (
+ 23}
+ renderItem={type => }
+ />
+ ) : (
+
+ {allTypes.map(type => (
- );
- })}
-
+ ))}
+
+ )}
>
);
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/section.css b/packages/graphiql-plugin-doc-explorer/src/components/section.css
index 3cc72bb874a..d7022c59153 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/section.css
+++ b/packages/graphiql-plugin-doc-explorer/src/components/section.css
@@ -20,3 +20,18 @@
margin-top: var(--px-16);
}
}
+
+.graphiql-doc-explorer-section:last-child {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ min-height: 0;
+}
+
+.graphiql-doc-explorer-section:last-child
+ > .graphiql-doc-explorer-section-content {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ min-height: 0;
+}
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/section.tsx b/packages/graphiql-plugin-doc-explorer/src/components/section.tsx
index c800c86428f..b3dea38d9fa 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/section.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/section.tsx
@@ -33,15 +33,23 @@ type ExplorerSectionProps = {
| 'Deprecated Enum Values'
| 'Directives'
| 'All Schema Types';
+ /**
+ * Optionally pass a classname for an ExplorerSection instance
+ */
+ className?: string;
};
export const ExplorerSection: FC = ({
title,
children,
+ className = '',
}) => {
const Icon = TYPE_TO_ICON[title];
+ const additionalClassName = className
+ ? ` ${className} graphiql-doc-explorer-section--${className}`
+ : '';
return (
-
+
{title}
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx b/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx
index 219ecf3488b..ec305994fec 100644
--- a/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx
+++ b/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx
@@ -219,11 +219,12 @@ const PossibleTypes: FC<{ type: GraphQLNamedType }> = ({ type }) => {
if (!schema || !isAbstractType(type)) {
return null;
}
+ const possibleTypes = schema.getPossibleTypes(type);
return (
- {schema.getPossibleTypes(type).map(possibleType => (
+ {possibleTypes.map(possibleType => (
diff --git a/packages/graphiql-plugin-doc-explorer/src/components/virtual-list.tsx b/packages/graphiql-plugin-doc-explorer/src/components/virtual-list.tsx
new file mode 100644
index 00000000000..e77b775fafe
--- /dev/null
+++ b/packages/graphiql-plugin-doc-explorer/src/components/virtual-list.tsx
@@ -0,0 +1,66 @@
+import { useVirtualizer } from '@tanstack/react-virtual';
+import type { ReactNode } from 'react';
+import { useState } from 'react';
+
+type VirtualListProps = {
+ items: readonly T[];
+ estimateSize: (index: number) => number;
+ renderItem: (item: T, index: number) => ReactNode;
+};
+
+export function VirtualList({
+ items,
+ estimateSize,
+ renderItem,
+}: VirtualListProps) {
+ // React Compiler memoizes this component's output. Because `virtualizer` is a
+ // stable instance, the compiler treats the render as cacheable and skips the
+ // re-renders that TanStack Virtual's internal useReducer dispatch triggers
+ // on scroll/resize. Disabling memoization here keeps scroll-driven updates working.
+ 'use no memo';
+
+ const [scrollEl, setScrollEl] = useState(null);
+
+ const virtualizer = useVirtualizer({
+ count: items.length,
+ getScrollElement: () => scrollEl,
+ estimateSize,
+ overscan: 5,
+ initialRect: { width: 0, height: 800 },
+ });
+
+ const virtualItems = virtualizer.getVirtualItems();
+
+ return (
+
+ {virtualItems.length > 0 ? (
+
+ {virtualItems.map(virtualRow => (
+
+ {renderItem(items[virtualRow.index]!, virtualRow.index)}
+
+ ))}
+
+ ) : (
+ items.map((item, index) => (
+
+ {renderItem(item, index)}
+
+ ))
+ )}
+
+ );
+}
diff --git a/resources/custom-words.txt b/resources/custom-words.txt
index ab691a503e4..66480c5fd63 100644
--- a/resources/custom-words.txt
+++ b/resources/custom-words.txt
@@ -42,9 +42,11 @@ bobbybobby
borggreve
bram
browserslistrc
+cacheable
calar
chainable
changesets
+classname
clsx
codebases
codegen
@@ -140,7 +142,6 @@ multipass
nauroze
newhope
nextjs
-nodenext
nishchit
nocheck
nocursor
@@ -159,6 +160,7 @@ orche
orta
outdir
outlineable
+overscan
ovsx
oxfmt
oxlint
@@ -214,6 +216,7 @@ svgo
svgr
tanay
tanaypratap
+tanstack
testid
testonly
therox
@@ -231,6 +234,7 @@ unfocus
unnormalized
unparsable
unsubscribable
+unvirtualized
urigo
urql
usememo
diff --git a/yarn.lock b/yarn.lock
index b5d2ba1697c..e1698aef15b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3035,6 +3035,7 @@ __metadata:
dependencies:
"@graphiql/react": "npm:^0.37.4"
"@headlessui/react": "npm:^2.2"
+ "@tanstack/react-virtual": "npm:^3.13.24"
"@testing-library/dom": "npm:^10.4.0"
"@testing-library/jest-dom": "npm:^6.6.3"
"@testing-library/react": "npm:^16.3.0"
@@ -6025,22 +6026,22 @@ __metadata:
languageName: node
linkType: hard
-"@tanstack/react-virtual@npm:^3.13.6":
- version: 3.13.6
- resolution: "@tanstack/react-virtual@npm:3.13.6"
+"@tanstack/react-virtual@npm:^3.13.24, @tanstack/react-virtual@npm:^3.13.6":
+ version: 3.13.24
+ resolution: "@tanstack/react-virtual@npm:3.13.24"
dependencies:
- "@tanstack/virtual-core": "npm:3.13.6"
+ "@tanstack/virtual-core": "npm:3.14.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- checksum: 10c0/3d2fdf024cb9189981cc015518a34a771a57cd698ca775022ad481431e6f26e596309f57d2238ef15f3b53b805879edaed6394e4cad1bb550ee92259e4d52633
+ checksum: 10c0/f409b2bb67965a513b75a1403e622c0b86c88c67419f757c79f670615979e38dc7ad5569a02c924741697df3d4301a0f45208fd4b9f935a5d58dd83e1db5622a
languageName: node
linkType: hard
-"@tanstack/virtual-core@npm:3.13.6":
- version: 3.13.6
- resolution: "@tanstack/virtual-core@npm:3.13.6"
- checksum: 10c0/9c40af4deccf0fadc5c371f4e659f1aff221db0ede9664ee7cf3642a2d05f6f1c9494605f3606f70d7c8948dc22e50c48519a8435d0e845f1b1f34c9353722c8
+"@tanstack/virtual-core@npm:3.14.0":
+ version: 3.14.0
+ resolution: "@tanstack/virtual-core@npm:3.14.0"
+ checksum: 10c0/9e07e7f74f5e02dfc47b358f7b5089e680d8b14b9c5b90e9497be6f57c76ca98d185fbe5008795b89919346bfc3676ebe1c61b34980eefe6674c88ec6ff4b136
languageName: node
linkType: hard