diff --git a/.changeset/famous-shirts-mate.md b/.changeset/famous-shirts-mate.md new file mode 100644 index 00000000000..72bf718cdd4 --- /dev/null +++ b/.changeset/famous-shirts-mate.md @@ -0,0 +1,5 @@ +--- +'graphiql': minor +--- + +Add a new prop to GraphiQL component: `forcedTheme` to force the theme and hide the theme switcher. diff --git a/.eslintrc.js b/.eslintrc.js index f9f7e78023c..15097833cb4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -359,6 +359,7 @@ module.exports = { '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', '@typescript-eslint/consistent-type-assertions': 'error', + '@typescript-eslint/no-duplicate-type-constituents': 'error', // TODO: Fix all errors for the following rules included in recommended config '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-non-null-assertion': 'off', diff --git a/packages/graphiql/cypress/e2e/theme.cy.ts b/packages/graphiql/cypress/e2e/theme.cy.ts new file mode 100644 index 00000000000..456b99511de --- /dev/null +++ b/packages/graphiql/cypress/e2e/theme.cy.ts @@ -0,0 +1,17 @@ +describe('Theme', () => { + it('Switches to light theme when `forcedTheme` is light', () => { + cy.visit('/?query={test}&forcedTheme=light'); + cy.get('body').should('have.class', 'graphiql-light'); + }); + + it('Switches to dark theme when `forcedTheme` is dark', () => { + cy.visit('/?query={test}&forcedTheme=dark'); + cy.get('body').should('have.class', 'graphiql-dark'); + }); + + it('Defaults to light theme when `forcedTheme` value is invalid', () => { + cy.visit('/?query={test}&forcedTheme=invalid'); + cy.get('[data-value=settings]').click(); + cy.get('.graphiql-dialog-section-title').eq(1).should('have.text', 'Theme'); // Check for the presence of the theme dialog + }); +}); diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index f78403dde30..b2fc32c7d25 100644 --- a/packages/graphiql/resources/renderExample.js +++ b/packages/graphiql/resources/renderExample.js @@ -102,5 +102,6 @@ root.render( shouldPersistHeaders: true, inputValueDeprecation: GraphQLVersion.includes('15.5') ? undefined : true, onTabChange, + forcedTheme: parameters.forcedTheme, }), ); diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index fb2a44522ff..5067d344515 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -14,6 +14,8 @@ import React, { ReactElement, useCallback, useState, + useEffect, + useMemo, } from 'react'; import { @@ -168,6 +170,7 @@ export function GraphiQL({ @@ -216,8 +219,16 @@ export type GraphiQLInterfaceProps = WriteableEditorProps & */ showPersistHeadersSettings?: boolean; disableTabs?: boolean; + /** + * forcedTheme allows enforcement of a specific theme for GraphiQL. + * This is useful when you want to make sure that GraphiQL is always + * rendered with a specific theme + */ + forcedTheme?: (typeof THEMES)[number]; }; +const THEMES = ['light', 'dark', 'system'] as const; + export function GraphiQLInterface(props: GraphiQLInterfaceProps) { const isHeadersEditorEnabled = props.isHeadersEditorEnabled ?? true; const editorContext = useEditorContext({ nonNull: true }); @@ -225,6 +236,13 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { const schemaContext = useSchemaContext({ nonNull: true }); const storageContext = useStorageContext(); const pluginContext = usePluginContext(); + const forcedTheme = useMemo( + () => + props.forcedTheme && THEMES.includes(props.forcedTheme) + ? props.forcedTheme + : undefined, + [props.forcedTheme], + ); const copy = useCopyQuery({ onCopyQuery: props.onCopyQuery }); const merge = useMergeQuery(); @@ -232,6 +250,14 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { const { theme, setTheme } = useTheme(); + useEffect(() => { + if (forcedTheme === 'system') { + setTheme(null); + } else if (forcedTheme === 'light' || forcedTheme === 'dark') { + setTheme(forcedTheme); + } + }, [forcedTheme, setTheme]); + const PluginContent = pluginContext?.visiblePlugin?.content; const pluginResize = useDragResize({ @@ -317,7 +343,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { - {props.toolbar?.additionalContent && props.toolbar.additionalContent} + {props.toolbar?.additionalContent} {props.toolbar?.additionalComponent && ( )} @@ -520,7 +546,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { )}
- {props.disableTabs ? null : ( + {!props.disableTabs && (
) : null} -
-
-
Theme
-
- Adjust how the interface looks like. + {!forcedTheme && ( +
+
+
Theme
+
+ Adjust how the interface appears. +
+ + + + +
- - - - - -
+ )} {storageContext ? (