From 988f98e95af02a1a517477093d8ad52546a4d60c Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 5 Jun 2026 16:47:19 -0700 Subject: [PATCH 1/2] Add theme / density / font-size settings to the settings dialog Fills in concrete preset values for `[data-density]` and `[data-font-size]` token blocks in `tokens.css`. Adds a new `SettingsDialog` component to `@graphiql/react` with `SegmentedControl`s for each axis, wired to `useGraphiQLSettings`. Settings persist via `localStorage`. The Monaco editor's font size follows the active preset. Wires `SettingsDialog` into the `graphiql` package's `ActivityBar`, replacing the inline settings UI there. Removes `forcedTheme` and `showPersistHeadersSettings` from `GraphiQLInterfaceProps`; both were deprecated props with no effect. --- .changeset/settings-dialog-presets.md | 8 + .../graphiql-react/src/components/index.ts | 2 + .../src/components/settings-dialog/index.css | 49 +++++ .../src/components/settings-dialog/index.tsx | 115 +++++++++++ .../settings-dialog.stories.tsx | 34 ++++ .../settings-dialog/settings-dialog.test.tsx | 187 ++++++++++++++++++ packages/graphiql-react/src/style/tokens.css | 86 +++++++- packages/graphiql/cypress/e2e/theme.cy.ts | 20 -- packages/graphiql/src/GraphiQL.spec.tsx | 47 ----- packages/graphiql/src/GraphiQL.tsx | 17 +- packages/graphiql/src/e2e.ts | 2 - packages/graphiql/src/ui/activity-bar.tsx | 178 +---------------- 12 files changed, 478 insertions(+), 267 deletions(-) create mode 100644 .changeset/settings-dialog-presets.md create mode 100644 packages/graphiql-react/src/components/settings-dialog/index.css create mode 100644 packages/graphiql-react/src/components/settings-dialog/index.tsx create mode 100644 packages/graphiql-react/src/components/settings-dialog/settings-dialog.stories.tsx create mode 100644 packages/graphiql-react/src/components/settings-dialog/settings-dialog.test.tsx diff --git a/.changeset/settings-dialog-presets.md b/.changeset/settings-dialog-presets.md new file mode 100644 index 00000000000..8194998c622 --- /dev/null +++ b/.changeset/settings-dialog-presets.md @@ -0,0 +1,8 @@ +--- +'@graphiql/react': minor +'graphiql': major +--- + +Add `SettingsDialog` component with theme, density, and font-size segmented controls backed by `useGraphiQLSettings`. Density and font-size presets fill in concrete token values for `[data-density]` and `[data-font-size]` blocks in `tokens.css`. Monaco editor font size follows the active font-size preset. + +**Breaking:** `GraphiQLInterfaceProps` no longer accepts `forcedTheme` or `showPersistHeadersSettings`. Both props were previously deprecated no-ops; remove them from any call sites. diff --git a/packages/graphiql-react/src/components/index.ts b/packages/graphiql-react/src/components/index.ts index a229d3cb622..21bce0ce9e9 100644 --- a/packages/graphiql-react/src/components/index.ts +++ b/packages/graphiql-react/src/components/index.ts @@ -39,3 +39,5 @@ export { ActivityRail } from './activity-rail'; export type { ActivityRailProps } from './activity-rail'; export { ResponseHeader } from './response-header'; export type { ResponseHeaderProps } from './response-header'; +export { SettingsDialog } from './settings-dialog'; +export type { SettingsDialogProps } from './settings-dialog'; diff --git a/packages/graphiql-react/src/components/settings-dialog/index.css b/packages/graphiql-react/src/components/settings-dialog/index.css new file mode 100644 index 00000000000..190fa3e21a2 --- /dev/null +++ b/packages/graphiql-react/src/components/settings-dialog/index.css @@ -0,0 +1,49 @@ +.graphiql-settings-dialog { + min-width: 360px; +} + +.graphiql-settings-dialog-header { + align-items: center; + border-bottom: 1px solid oklch(var(--border-default)); + display: flex; + justify-content: space-between; + padding: var(--px-12) var(--px-4) var(--px-12) var(--px-16); +} + +.graphiql-settings-dialog-title { + color: oklch(var(--fg-strong)); + font-family: var(--font-family); + font-size: var(--font-size-body); + font-weight: 600; + margin: 0; +} + +.graphiql-settings-dialog-body { + display: flex; + flex-direction: column; + padding: var(--px-4) 0; +} + +.graphiql-settings-section { + align-items: center; + border-bottom: 1px solid oklch(var(--border-muted)); + display: flex; + gap: var(--px-16); + justify-content: space-between; + padding: var(--px-12) var(--px-16); +} + +.graphiql-settings-section:last-child { + border-bottom: none; +} + +.graphiql-settings-section-title { + color: oklch(var(--fg-default)); + flex-shrink: 0; + font-family: var(--font-family); + font-size: var(--font-size-small); + font-weight: 500; + letter-spacing: var(--letter-spacing-ui); + margin: 0; + min-width: 72px; +} diff --git a/packages/graphiql-react/src/components/settings-dialog/index.tsx b/packages/graphiql-react/src/components/settings-dialog/index.tsx new file mode 100644 index 00000000000..46956c6fb94 --- /dev/null +++ b/packages/graphiql-react/src/components/settings-dialog/index.tsx @@ -0,0 +1,115 @@ +'use no memo'; + +import type { FC } from 'react'; +import { Dialog } from '../dialog'; +import { SegmentedControl } from '../segmented-control'; +import { + useGraphiQLSettings, + type Theme, + type Density, + type FontSize, +} from '../../hooks/use-graphiql-settings'; +import { useMonaco } from '../../stores'; +import { useEffect } from 'react'; +import './index.css'; + +const FONT_SIZE_PX: Record = { + compact: 12, + default: 13, + large: 14, + xl: 16, +}; + +const THEME_OPTIONS: { value: Theme; label: string }[] = [ + { value: 'auto', label: 'Auto' }, + { value: 'light', label: 'Light' }, + { value: 'dark', label: 'Dark' }, +]; + +const DENSITY_OPTIONS: { value: Density; label: string }[] = [ + { value: 'compact', label: 'Compact' }, + { value: 'comfortable', label: 'Comfortable' }, + { value: 'spacious', label: 'Spacious' }, +]; + +const FONT_SIZE_OPTIONS: { value: FontSize; label: string }[] = [ + { value: 'compact', label: 'Compact' }, + { value: 'default', label: 'Default' }, + { value: 'large', label: 'Large' }, + { value: 'xl', label: 'Extra Large' }, +]; + +export interface SettingsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +/** + * Settings dialog with controls for theme, density, and font size. + * Reads and writes settings via `useGraphiQLSettings`. Monaco editor + * font size is updated to match the active font-size preset. + */ +export const SettingsDialog: FC = ({ + open, + onOpenChange, +}) => { + const { theme, setTheme, density, setDensity, fontSize, setFontSize } = + useGraphiQLSettings(); + const monaco = useMonaco(state => state.monaco); + + // Keep Monaco editor font size in sync with the active preset. + useEffect(() => { + if (!monaco) { + return; + } + const px = FONT_SIZE_PX[fontSize]; + monaco.editor.getEditors().forEach(editor => { + editor.updateOptions({ fontSize: px }); + }); + }, [fontSize, monaco]); + + return ( + +
+
+ + Settings + + +
+ +
+
+

Theme

+ +
+ +
+

Density

+ +
+ +
+

Font size

+ +
+
+
+
+ ); +}; diff --git a/packages/graphiql-react/src/components/settings-dialog/settings-dialog.stories.tsx b/packages/graphiql-react/src/components/settings-dialog/settings-dialog.stories.tsx new file mode 100644 index 00000000000..caf92dcafd1 --- /dev/null +++ b/packages/graphiql-react/src/components/settings-dialog/settings-dialog.stories.tsx @@ -0,0 +1,34 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Button } from '../button'; +import { SettingsDialog } from './index'; + +const meta: Meta = { + title: 'Components/SettingsDialog', + component: SettingsDialog, + parameters: { + layout: 'centered', + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: function DefaultStory() { + const [open, setOpen] = useState(false); + return ( + <> + + + + ); + }, +}; + +export const AlwaysOpen: Story = { + render: function AlwaysOpenStory() { + return undefined} />; + }, +}; diff --git a/packages/graphiql-react/src/components/settings-dialog/settings-dialog.test.tsx b/packages/graphiql-react/src/components/settings-dialog/settings-dialog.test.tsx new file mode 100644 index 00000000000..eaca83cee97 --- /dev/null +++ b/packages/graphiql-react/src/components/settings-dialog/settings-dialog.test.tsx @@ -0,0 +1,187 @@ +import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'; +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { SettingsDialog } from './index'; +import { SETTINGS_STORAGE_KEY } from '../../hooks/use-graphiql-settings'; + +// The Monaco store performs dynamic imports of monaco-editor / monaco-graphql +// that don't resolve in jsdom. Stub it out so the dialog can render without +// a real editor environment. +vi.mock('../../stores/monaco', () => ({ + monacoStore: { + getState: () => ({ monaco: undefined, monacoGraphQL: undefined }), + subscribe: () => () => undefined, + }, + useMonaco: (selector: (state: { monaco: undefined }) => unknown) => + selector({ monaco: undefined }), +})); + +// Polyfill localStorage for environments that lack it. +beforeAll(() => { + if (globalThis.localStorage === undefined) { + const store = new Map(); + const storage: Storage = { + getItem: (key: string) => store.get(key) ?? null, + setItem(key: string, value: string) { + store.set(key, value); + }, + removeItem(key: string) { + store.delete(key); + }, + clear() { + store.clear(); + }, + key: (index: number) => Array.from(store.keys())[index] ?? null, + get length() { + return store.size; + }, + }; + Object.defineProperty(window, 'localStorage', { + writable: true, + value: storage, + }); + } + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: (query: string): MediaQueryList => + ({ + matches: false, + media: query, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }) as unknown as MediaQueryList, + }); +}); + +beforeEach(() => { + localStorage.removeItem(SETTINGS_STORAGE_KEY); + // Ensure a graphiql-container exists for useGraphiQLSettings' DOM effect. + const existing = document.querySelector('.graphiql-container'); + if (!existing) { + const c = document.createElement('div'); + c.className = 'graphiql-container'; + document.body.append(c); + } +}); + +function renderDialog(open = true) { + const onOpenChange = vi.fn(); + render(); + return { onOpenChange }; +} + +describe('SettingsDialog — renders', () => { + it('shows the Settings title when open', () => { + renderDialog(); + expect(screen.getByText('Settings')).toBeInTheDocument(); + }); + + it('shows Theme, Density, and Font size section headings', () => { + renderDialog(); + // Each section renders an h3 title and a fieldset legend — query by role. + expect(screen.getAllByText('Theme').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('Density').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('Font size').length).toBeGreaterThanOrEqual(1); + // Confirm the section headings are present as h3 elements. + const headings = screen.getAllByRole('heading', { level: 3 }); + const headingTexts = headings.map(h => h.textContent); + expect(headingTexts).toContain('Theme'); + expect(headingTexts).toContain('Density'); + expect(headingTexts).toContain('Font size'); + }); +}); + +describe('SettingsDialog — theme control', () => { + it('renders Auto / Light / Dark options', () => { + renderDialog(); + const fieldset = screen + .getByRole('group', { name: 'Theme' }); + expect(within(fieldset).getByRole('radio', { name: 'Auto' })).toBeInTheDocument(); + expect(within(fieldset).getByRole('radio', { name: 'Light' })).toBeInTheDocument(); + expect(within(fieldset).getByRole('radio', { name: 'Dark' })).toBeInTheDocument(); + }); + + it('defaults to Auto', () => { + renderDialog(); + const auto = screen.getByRole('radio', { name: 'Auto' }); + expect(auto).toBeChecked(); + }); + + it('switches theme to Dark on click', async () => { + const user = userEvent.setup(); + renderDialog(); + const dark = screen.getByRole('radio', { name: 'Dark' }); + await user.click(dark); + expect(dark).toBeChecked(); + const stored = JSON.parse(localStorage.getItem(SETTINGS_STORAGE_KEY)!); + expect(stored.theme).toBe('dark'); + }); +}); + +describe('SettingsDialog — density control', () => { + it('renders Compact / Comfortable / Spacious options', () => { + renderDialog(); + const fieldset = screen.getByRole('group', { name: 'Density' }); + expect(within(fieldset).getByRole('radio', { name: 'Compact' })).toBeInTheDocument(); + expect(within(fieldset).getByRole('radio', { name: 'Comfortable' })).toBeInTheDocument(); + expect(within(fieldset).getByRole('radio', { name: 'Spacious' })).toBeInTheDocument(); + }); + + it('defaults to Comfortable', () => { + renderDialog(); + expect(screen.getByRole('radio', { name: 'Comfortable' })).toBeChecked(); + }); + + it('switches density to Compact on click', async () => { + const user = userEvent.setup(); + renderDialog(); + const fieldset = screen.getByRole('group', { name: 'Density' }); + const compact = within(fieldset).getByRole('radio', { name: 'Compact' }); + await user.click(compact); + expect(compact).toBeChecked(); + const stored = JSON.parse(localStorage.getItem(SETTINGS_STORAGE_KEY)!); + expect(stored.density).toBe('compact'); + }); +}); + +describe('SettingsDialog — font size control', () => { + it('renders Compact / Default / Large / Extra Large options', () => { + renderDialog(); + const fieldset = screen.getByRole('group', { name: 'Font size' }); + expect( + within(fieldset).getByRole('radio', { name: 'Compact' }), + ).toBeInTheDocument(); + expect( + within(fieldset).getByRole('radio', { name: 'Default' }), + ).toBeInTheDocument(); + expect( + within(fieldset).getByRole('radio', { name: 'Large' }), + ).toBeInTheDocument(); + expect( + within(fieldset).getByRole('radio', { name: 'Extra Large' }), + ).toBeInTheDocument(); + }); + + it('defaults to Default', () => { + renderDialog(); + expect(screen.getByRole('radio', { name: 'Default' })).toBeChecked(); + }); + + it('switches font size to Large on click', async () => { + const user = userEvent.setup(); + renderDialog(); + const large = screen.getByRole('radio', { name: 'Large' }); + await user.click(large); + expect(large).toBeChecked(); + const stored = JSON.parse(localStorage.getItem(SETTINGS_STORAGE_KEY)!); + expect(stored.fontSize).toBe('large'); + }); +}); + +describe('SettingsDialog — closed state', () => { + it('does not render content when closed', () => { + renderDialog(false); + expect(screen.queryByText('Settings')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/graphiql-react/src/style/tokens.css b/packages/graphiql-react/src/style/tokens.css index 8cc2d870160..15403960472 100644 --- a/packages/graphiql-react/src/style/tokens.css +++ b/packages/graphiql-react/src/style/tokens.css @@ -49,11 +49,7 @@ /* Typography */ --font-family: 'Inter', ui-sans-serif, system-ui, sans-serif; --font-family-mono: 'JetBrains Mono', ui-monospace, monospace; - --font-size-body: 13px; - --font-size-small: 11px; - --font-size-mono: 12px; --font-size-eyebrow: 10.5px; - --line-height-body: 18px; --letter-spacing-ui: -0.005em; /* Spacing scale */ @@ -67,12 +63,8 @@ --px-24: 24px; --px-32: 32px; - /* Layout dimensions */ - --top-bar-height: 40px; - --status-bar-height: 24px; - --rail-width: 48px; + /* Layout dimensions (density-controlled; comfortable is the default) */ --side-panel-width: 340px; - --tab-strip-height: 32px; /* Radii */ --radius-sm: 4px; @@ -125,6 +117,82 @@ /* Typography, spacing, dimensions, and radii are inherited from :root. */ } +/* ------------------------------------------------------------------------- + * Density presets + * + * `comfortable` is the default; the combined selector makes it the fallback + * when the attribute is absent. + * ---------------------------------------------------------------------- */ + +:root, +[data-density='comfortable'] { + --row-padding-y: 5px; + --row-padding-x: 14px; + --button-padding-y: 4px; + --button-padding-x: 10px; + --top-bar-height: 40px; + --status-bar-height: 24px; + --rail-width: 48px; + --tab-strip-height: 32px; +} + +[data-density='compact'] { + --row-padding-y: 3px; + --row-padding-x: 12px; + --button-padding-y: 2px; + --button-padding-x: 8px; + --top-bar-height: 36px; + --status-bar-height: 22px; + --rail-width: 44px; + --tab-strip-height: 28px; +} + +[data-density='spacious'] { + --row-padding-y: 7px; + --row-padding-x: 16px; + --button-padding-y: 6px; + --button-padding-x: 12px; + --top-bar-height: 44px; + --status-bar-height: 28px; + --rail-width: 52px; + --tab-strip-height: 36px; +} + +/* ------------------------------------------------------------------------- + * Font-size presets + * + * `default` doubles as the fallback via the combined selector. + * ---------------------------------------------------------------------- */ + +:root, +[data-font-size='default'] { + --font-size-body: 13px; + --font-size-small: 11px; + --font-size-mono: 12px; + --line-height-body: 18px; +} + +[data-font-size='compact'] { + --font-size-body: 12px; + --font-size-small: 11px; + --font-size-mono: 11.5px; + --line-height-body: 16px; +} + +[data-font-size='large'] { + --font-size-body: 14px; + --font-size-small: 12px; + --font-size-mono: 13px; + --line-height-body: 20px; +} + +[data-font-size='xl'] { + --font-size-body: 16px; + --font-size-small: 14px; + --font-size-mono: 14px; + --line-height-body: 22px; +} + /* * Apply light overrides automatically when the system prefers light and the * user has not pinned dark. The values are duplicated rather than relying on diff --git a/packages/graphiql/cypress/e2e/theme.cy.ts b/packages/graphiql/cypress/e2e/theme.cy.ts index d9b751f19bf..721ab8c9684 100644 --- a/packages/graphiql/cypress/e2e/theme.cy.ts +++ b/packages/graphiql/cypress/e2e/theme.cy.ts @@ -1,24 +1,4 @@ describe('Theme', () => { - describe('`forcedTheme`', () => { - it('Switches to light theme when `forcedTheme` is light', () => { - cy.visit('?forcedTheme=light'); - cy.get('body').should('have.class', 'graphiql-light'); - }); - - it('Switches to dark theme when `forcedTheme` is dark', () => { - cy.visit('?forcedTheme=dark'); - cy.get('body').should('have.class', 'graphiql-dark'); - }); - - it('Defaults to light theme when `forcedTheme` value is invalid', () => { - cy.visit('?forcedTheme=invalid'); - cy.get('.graphiql-activity-rail-settings').click(); - cy.get('.graphiql-dialog-section-title') - .eq(1) - .should('have.text', 'Theme'); // Check for the presence of the theme dialog - }); - }); - describe('`defaultTheme`', () => { it('should have light theme', () => { cy.visit('?defaultTheme=light'); diff --git a/packages/graphiql/src/GraphiQL.spec.tsx b/packages/graphiql/src/GraphiQL.spec.tsx index 66093bc6f9d..2d440ea19bd 100644 --- a/packages/graphiql/src/GraphiQL.spec.tsx +++ b/packages/graphiql/src/GraphiQL.spec.tsx @@ -411,53 +411,6 @@ describe('GraphiQL', () => { }); }); // panel resizing - it('allows the user to control persisting headers if it is true', async () => { - const { container, findByText } = render( - , - ); - - act(() => { - fireEvent.click(container.querySelector('[aria-label="Settings"]')!); - }); - - const element = await findByText('Persist headers'); - expect(element).toBeInTheDocument(); - }); - - it('allows the user to control persisting headers if it is not passed in', async () => { - const { container, findByText } = render( - , - ); - - act(() => { - fireEvent.click(container.querySelector('[aria-label="Settings"]')!); - }); - - const element = await findByText('Persist headers'); - expect(element).toBeInTheDocument(); - }); - - it('does not allow the user to control persisting headers is false', async () => { - const { container, findByText } = render( - , - ); - - act(() => { - fireEvent.click(container.querySelector('[aria-label="Settings"]')!); - }); - - const callback = async () => { - try { - // Expecting non-existence; short-circuit instead of waiting the default. - await findByText('Persist headers', undefined, { timeout: 1000 }); - } catch { - // eslint-disable-next-line no-throw-literal - throw 'failed'; - } - }; - await expect(callback).rejects.toEqual('failed'); - }); - describe('Tabs', () => { it('show tabs', async () => { const { container } = render(); diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index 3845e01ff20..395a3a56544 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -90,11 +90,8 @@ const GraphiQL_: FC = ({ responseTooltip, defaultEditorToolsVisibility, isHeadersEditorEnabled, - showPersistHeadersSettings, - forcedTheme, confirmCloseTab, className, - children, ...props }) => { @@ -121,16 +118,12 @@ const GraphiQL_: FC = ({ throw new TypeError('The `readOnly` prop has been removed.'); } const interfaceProps: GraphiQLInterfaceProps = { - // TODO check if `showPersistHeadersSettings` prop is needed, or we can just use `shouldPersistHeaders` instead - showPersistHeadersSettings: - showPersistHeadersSettings ?? props.shouldPersistHeaders !== false, onEditQuery, onEditVariables, onEditHeaders, responseTooltip, defaultEditorToolsVisibility, isHeadersEditorEnabled, - forcedTheme, confirmCloseTab, className, }; @@ -169,11 +162,7 @@ export interface GraphiQLInterfaceProps AddSuffix, 'Query'>, AddSuffix, 'Variables'>, AddSuffix, 'Headers'>, - Pick, - Pick< - ComponentPropsWithoutRef, - 'forcedTheme' | 'showPersistHeadersSettings' - > { + Pick { children?: ReactNode; /** * Set the default state for the editor tools. @@ -217,7 +206,6 @@ const LABEL = { }; export const GraphiQLInterface: FC = ({ - forcedTheme, isHeadersEditorEnabled = true, defaultEditorToolsVisibility, children: $children, @@ -227,7 +215,6 @@ export const GraphiQLInterface: FC = ({ onEditVariables, onEditHeaders, responseTooltip, - showPersistHeadersSettings, }) => { const { addTab, @@ -481,8 +468,6 @@ export const GraphiQLInterface: FC = ({
diff --git a/packages/graphiql/src/e2e.ts b/packages/graphiql/src/e2e.ts index b422ac99bb6..228e7041fb2 100644 --- a/packages/graphiql/src/e2e.ts +++ b/packages/graphiql/src/e2e.ts @@ -28,7 +28,6 @@ interface Params { confirmCloseTab?: 'true'; onPrettifyQuery?: 'true'; - forcedTheme?: 'light' | 'dark' | 'system'; defaultTheme?: Theme; } @@ -125,7 +124,6 @@ const props: ComponentProps = { onPrettifyQuery: parameters.onPrettifyQuery === 'true' ? onPrettifyQuery : undefined, onTabChange, - forcedTheme: parameters.forcedTheme, defaultTheme: parameters.defaultTheme, }; diff --git a/packages/graphiql/src/ui/activity-bar.tsx b/packages/graphiql/src/ui/activity-bar.tsx index b4525177ea5..8d703cf48a4 100644 --- a/packages/graphiql/src/ui/activity-bar.tsx +++ b/packages/graphiql/src/ui/activity-bar.tsx @@ -1,68 +1,23 @@ -import { type FC, type MouseEventHandler, useEffect, useState } from 'react'; +import { type FC, useEffect, useState } from 'react'; import { ActivityRail, - Button, - ButtonGroup, - cn, Dialog, isMacOs, - pick, - useGraphiQL, - useGraphiQLActions, + SettingsDialog, useDragResize, VisuallyHidden, type GraphiQLPlugin, } from '@graphiql/react'; import { ShortKeys } from './short-keys'; -const THEMES = ['light', 'dark', 'system'] as const; - export interface ActivityBarProps { - /** - * `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]; - - /** - * Indicates if settings for persisting headers should appear in the - * settings modal. - */ - showPersistHeadersSettings?: boolean; - setHiddenElement: ReturnType['setHiddenElement']; } -type ButtonHandler = MouseEventHandler; - -export const ActivityBar: FC = ({ - forcedTheme: $forcedTheme, - showPersistHeadersSettings, - setHiddenElement, -}) => { - const forcedTheme = - $forcedTheme && THEMES.includes($forcedTheme) ? $forcedTheme : undefined; - - const { setShouldPersistHeaders, setTheme } = useGraphiQLActions(); - const { shouldPersistHeaders, theme, storage } = useGraphiQL( - pick('shouldPersistHeaders', 'theme', 'storage'), - ); - - useEffect(() => { - if (forcedTheme === 'system') { - setTheme(null); - } else if (forcedTheme === 'light' || forcedTheme === 'dark') { - setTheme(forcedTheme); - } - }, [forcedTheme, setTheme]); - +export const ActivityBar: FC = ({ setHiddenElement }) => { const [showDialog, setShowDialog] = useState< 'settings' | 'short-keys' | null >(null); - const [clearStorageStatus, setClearStorageStatus] = useState< - 'success' | 'error' | undefined - >(); useEffect(() => { function openSettings(event: KeyboardEvent) { @@ -80,7 +35,6 @@ export const ActivityBar: FC = ({ function handleOpenSettingsDialog(isOpen: boolean) { if (!isOpen) { setShowDialog(null); - setClearStorageStatus(undefined); } } @@ -90,27 +44,6 @@ export const ActivityBar: FC = ({ } } - function handleClearData() { - try { - storage.clear(); - setClearStorageStatus('success'); - } catch { - setClearStorageStatus('error'); - } - } - - const handlePersistHeaders: ButtonHandler = event => { - setShouldPersistHeaders(event.currentTarget.dataset.value === 'true'); - }; - - const handleChangeTheme: ButtonHandler = event => { - const selectedTheme = event.currentTarget.dataset.theme as - | 'light' - | 'dark' - | undefined; - setTheme(selectedTheme || null); - }; - function handlePluginToggle(nextPlugin: GraphiQLPlugin | null) { if (nextPlugin === null) { setHiddenElement('first'); @@ -145,111 +78,10 @@ export const ActivityBar: FC = ({
- -
- - Settings - - - - This modal lets you adjust header persistence, interface theme, - and clear local storage. - - - -
- {showPersistHeadersSettings ? ( -
-
-
- Persist headers -
-
- Save headers upon reloading.{' '} - - Only enable if you trust this device. - -
-
- - - - -
- ) : null} - {!forcedTheme && ( -
-
-
Theme
-
- Adjust how the interface appears. -
-
- - - - - -
- )} -
-
-
Clear storage
-
- Remove all locally stored data and start fresh. -
-
- -
-
+ /> ); }; From a7baf114493097d7d7dce69fcafc6c88f69fc8a3 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Sat, 6 Jun 2026 10:35:49 -0700 Subject: [PATCH 2/2] lint and format fixes --- .../settings-dialog.stories.tsx | 2 +- .../settings-dialog/settings-dialog.test.tsx | 29 +++++++++++++------ packages/graphiql/src/GraphiQL.tsx | 4 +-- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/graphiql-react/src/components/settings-dialog/settings-dialog.stories.tsx b/packages/graphiql-react/src/components/settings-dialog/settings-dialog.stories.tsx index caf92dcafd1..b9b0c2be031 100644 --- a/packages/graphiql-react/src/components/settings-dialog/settings-dialog.stories.tsx +++ b/packages/graphiql-react/src/components/settings-dialog/settings-dialog.stories.tsx @@ -29,6 +29,6 @@ export const Default: Story = { export const AlwaysOpen: Story = { render: function AlwaysOpenStory() { - return undefined} />; + return {}} />; }, }; diff --git a/packages/graphiql-react/src/components/settings-dialog/settings-dialog.test.tsx b/packages/graphiql-react/src/components/settings-dialog/settings-dialog.test.tsx index eaca83cee97..7536fec648d 100644 --- a/packages/graphiql-react/src/components/settings-dialog/settings-dialog.test.tsx +++ b/packages/graphiql-react/src/components/settings-dialog/settings-dialog.test.tsx @@ -10,7 +10,7 @@ import { SETTINGS_STORAGE_KEY } from '../../hooks/use-graphiql-settings'; vi.mock('../../stores/monaco', () => ({ monacoStore: { getState: () => ({ monaco: undefined, monacoGraphQL: undefined }), - subscribe: () => () => undefined, + subscribe: () => () => {}, }, useMonaco: (selector: (state: { monaco: undefined }) => unknown) => selector({ monaco: undefined }), @@ -95,11 +95,16 @@ describe('SettingsDialog — renders', () => { describe('SettingsDialog — theme control', () => { it('renders Auto / Light / Dark options', () => { renderDialog(); - const fieldset = screen - .getByRole('group', { name: 'Theme' }); - expect(within(fieldset).getByRole('radio', { name: 'Auto' })).toBeInTheDocument(); - expect(within(fieldset).getByRole('radio', { name: 'Light' })).toBeInTheDocument(); - expect(within(fieldset).getByRole('radio', { name: 'Dark' })).toBeInTheDocument(); + const fieldset = screen.getByRole('group', { name: 'Theme' }); + expect( + within(fieldset).getByRole('radio', { name: 'Auto' }), + ).toBeInTheDocument(); + expect( + within(fieldset).getByRole('radio', { name: 'Light' }), + ).toBeInTheDocument(); + expect( + within(fieldset).getByRole('radio', { name: 'Dark' }), + ).toBeInTheDocument(); }); it('defaults to Auto', () => { @@ -123,9 +128,15 @@ describe('SettingsDialog — density control', () => { it('renders Compact / Comfortable / Spacious options', () => { renderDialog(); const fieldset = screen.getByRole('group', { name: 'Density' }); - expect(within(fieldset).getByRole('radio', { name: 'Compact' })).toBeInTheDocument(); - expect(within(fieldset).getByRole('radio', { name: 'Comfortable' })).toBeInTheDocument(); - expect(within(fieldset).getByRole('radio', { name: 'Spacious' })).toBeInTheDocument(); + expect( + within(fieldset).getByRole('radio', { name: 'Compact' }), + ).toBeInTheDocument(); + expect( + within(fieldset).getByRole('radio', { name: 'Comfortable' }), + ).toBeInTheDocument(); + expect( + within(fieldset).getByRole('radio', { name: 'Spacious' }), + ).toBeInTheDocument(); }); it('defaults to Comfortable', () => { diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index 395a3a56544..f8a43174949 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -467,9 +467,7 @@ export const GraphiQLInterface: FC = ({
- +