diff --git a/.changeset/lovely-bobcats-lie.md b/.changeset/lovely-bobcats-lie.md new file mode 100644 index 0000000000..6c0522d28b --- /dev/null +++ b/.changeset/lovely-bobcats-lie.md @@ -0,0 +1,5 @@ +--- +'graphiql': minor +--- + +allow reassign name of the tabs diff --git a/packages/graphiql-react/src/ui/tabs.css b/packages/graphiql-react/src/ui/tabs.css index 66cd5b7336..87f8e9996b 100644 --- a/packages/graphiql-react/src/ui/tabs.css +++ b/packages/graphiql-react/src/ui/tabs.css @@ -16,12 +16,34 @@ min-width: 0; } +.graphiql-tab-input { + all: unset; + width: 100%; + padding: var(--px-4) 28px var(--px-4) var(--px-8); + border-radius: var(--border-radius-8) var(--border-radius-8) 0 0; + + &:focus { + outline: hsla(var(--color-neutral), var(--alpha-background-heavy)) auto 1px; + } + + &[readonly] { + cursor: pointer; + /* remove selection when focusing */ + &::selection { + user-select: none; + } + } +} + +.graphiql-tab:has(input[readonly]) { + max-width: 140px; +} + .graphiql-tab { border-radius: var(--border-radius-8) var(--border-radius-8) 0 0; background: hsla(var(--color-neutral), var(--alpha-background-light)); position: relative; display: flex; - max-width: 140px; /* disable shrinking while changing the operation name */ &:not(:focus-within) { diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 58d035e6e4..82ebad0602 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -19,6 +19,9 @@ import React, { Children, JSX, cloneElement, + MouseEvent, + FocusEvent, + ComponentProps, } from 'react'; import { @@ -240,6 +243,33 @@ const THEMES = ['light', 'dark', 'system'] as const; const TAB_CLASS_PREFIX = 'graphiql-session-tab-'; +const handleTabValueChange: ComponentProps<'input'>['onChange'] = event => { + const input = event.target; + // Should be at least 1 character wide, otherwise will throw DOMException + input.size = Math.max(1, input.value.length - 1); +}; + +const handleTabValueKeyDown: ComponentProps<'input'>['onKeyDown'] = event => { + if (event.key !== 'Enter' && event.key !== 'Escape') { + return; + } + const input = event.currentTarget; + + if (!input.value) { + input.value = input.defaultValue; + // @ts-expect-error + handleTabValueChange(event); + } + input.blur(); +}; + +const handleTabDoubleClickAndBlur = ( + event: MouseEvent<HTMLInputElement> | FocusEvent<HTMLInputElement>, +) => { + const input = event.currentTarget; + input.readOnly = event.type === 'blur'; +}; + export function GraphiQLInterface(props: GraphiQLInterfaceProps) { const isHeadersEditorEnabled = props.isHeadersEditorEnabled ?? true; const editorContext = useEditorContext({ nonNull: true }); @@ -490,7 +520,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { [confirmClose, editorContext, executionContext], ); - const handleTabClick: MouseEventHandler<HTMLButtonElement> = useCallback( + const handleTabClick: MouseEventHandler<HTMLInputElement> = useCallback( event => { const index = Number( event.currentTarget.id.replace(TAB_CLASS_PREFIX, ''), @@ -594,14 +624,25 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { value={tab} isActive={index === editorContext.activeTabIndex} > - <Tab.Button + <input + key={tab.title} + className="graphiql-tab-input" aria-controls="graphiql-session" id={`graphiql-session-tab-${index}`} title={tab.title} + defaultValue={tab.title} + size={Math.max(tab.title.length - 1, 1)} + onChange={handleTabValueChange} + onKeyDown={handleTabValueKeyDown} + onDoubleClick={handleTabDoubleClickAndBlur} + onBlur={handleTabDoubleClickAndBlur} onClick={handleTabClick} - > - {tab.title} - </Tab.Button> + // Can be writable only after double click + readOnly + // Disable autocomplete for tab names + autoComplete="off" + /> + {tabs.length > 1 && <Tab.Close onClick={handleTabClose} />} </Tab> ))} diff --git a/packages/graphiql/vite.config.mts b/packages/graphiql/vite.config.mts index f6f830074f..fef6ea0418 100644 --- a/packages/graphiql/vite.config.mts +++ b/packages/graphiql/vite.config.mts @@ -1,7 +1,6 @@ import { defineConfig, PluginOption } from 'vite'; import packageJSON from './package.json'; import dts from 'vite-plugin-dts'; -import commonjs from 'vite-plugin-commonjs'; const umdConfig = defineConfig({ define: { @@ -11,8 +10,6 @@ const umdConfig = defineConfig({ 'globalThis.process': 'true', 'process.env.NODE_ENV': '"production"', }, - // To bundle `const { createClient } = require('graphql-ws')` in `createWebsocketsFetcherFromUrl` function - plugins: [commonjs()], build: { minify: 'terser', // produce less bundle size sourcemap: true,