diff --git a/packages/graphiql-react/src/icons/docs.svg b/packages/graphiql-react/src/icons/docs.svg new file mode 100644 index 00000000000..4c2bf68a40c --- /dev/null +++ b/packages/graphiql-react/src/icons/docs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/history.svg b/packages/graphiql-react/src/icons/history.svg new file mode 100644 index 00000000000..3a632094812 --- /dev/null +++ b/packages/graphiql-react/src/icons/history.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/graphiql-react/src/icons/index.tsx b/packages/graphiql-react/src/icons/index.tsx index fad2253d041..182b6c0f379 100644 --- a/packages/graphiql-react/src/icons/index.tsx +++ b/packages/graphiql-react/src/icons/index.tsx @@ -1,9 +1,14 @@ import _ChevronDownIcon from './chevron-down.svg'; import _ChevronUpIcon from './chevron-up.svg'; import _CopyIcon from './copy.svg'; +import _DocsIcon from './docs.svg'; +import _HistoryIcon from './history.svg'; +import _KeyboardShortcutIcon from './keyboard-shortcut.svg'; import _MergeIcon from './merge.svg'; import _PlayIcon from './play.svg'; import _PrettifyIcon from './prettify.svg'; +import _ReloadIcon from './reload.svg'; +import _SettingsIcon from './settings.svg'; import _StopIcon from './stop.svg'; export const ChevronDownIcon = generateIcon( @@ -12,9 +17,17 @@ export const ChevronDownIcon = generateIcon( ); export const ChevronUpIcon = generateIcon(_ChevronUpIcon, 'chevron up icon'); export const CopyIcon = generateIcon(_CopyIcon, 'copy icon'); +export const DocsIcon = generateIcon(_DocsIcon, 'docs icon'); +export const HistoryIcon = generateIcon(_HistoryIcon, 'history icon'); +export const KeyboardShortcutIcon = generateIcon( + _KeyboardShortcutIcon, + 'keyboard shortcut icon', +); export const MergeIcon = generateIcon(_MergeIcon, 'merge icon'); export const PlayIcon = generateIcon(_PlayIcon, 'play icon'); export const PrettifyIcon = generateIcon(_PrettifyIcon, 'prettify icon'); +export const ReloadIcon = generateIcon(_ReloadIcon, 'reload icon'); +export const SettingsIcon = generateIcon(_SettingsIcon, 'settings icon'); export const StopIcon = generateIcon(_StopIcon, 'stop icon'); function generateIcon(RawComponent: any, title: string) { diff --git a/packages/graphiql-react/src/icons/keyboard-shortcut.svg b/packages/graphiql-react/src/icons/keyboard-shortcut.svg new file mode 100644 index 00000000000..8d192e164d1 --- /dev/null +++ b/packages/graphiql-react/src/icons/keyboard-shortcut.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/graphiql-react/src/icons/reload.svg b/packages/graphiql-react/src/icons/reload.svg new file mode 100644 index 00000000000..b0963c11a7e --- /dev/null +++ b/packages/graphiql-react/src/icons/reload.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/graphiql-react/src/icons/settings.svg b/packages/graphiql-react/src/icons/settings.svg new file mode 100644 index 00000000000..f7cf68be0a1 --- /dev/null +++ b/packages/graphiql-react/src/icons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/graphiql-react/src/style/root.css b/packages/graphiql-react/src/style/root.css index 7b2ce696141..64c79c9dc7d 100644 --- a/packages/graphiql-react/src/style/root.css +++ b/packages/graphiql-react/src/style/root.css @@ -54,6 +54,7 @@ 0px 0.399006px 1.33002px rgba(59, 76, 106, 0.0525061); /* Layout */ + --sidebar-width: 44px; --toolbar-width: 40px; } @@ -61,6 +62,7 @@ .CodeMirror-info, .CodeMirror-lint-tooltip, .graphiql-container button { + color: var(--color-neutral-100); font-family: var(--font-family); font-size: var(--font-size-body); font-weight: var(----font-weight-regular); diff --git a/packages/graphiql-react/src/utility/resize.tsx b/packages/graphiql-react/src/utility/resize.tsx index 43466fdc27c..5fc6f0f087f 100644 --- a/packages/graphiql-react/src/utility/resize.tsx +++ b/packages/graphiql-react/src/utility/resize.tsx @@ -145,12 +145,8 @@ export function useDragResize({ if (firstRef.current && storage && storageKey) { const storedValue = storage?.get(storageKey); - if ( - storedValue && - storedValue !== HIDE_FIRST && - storedValue !== HIDE_SECOND - ) { - firstRef.current.style.flex = storedValue; + if (storedValue !== HIDE_FIRST && storedValue !== HIDE_SECOND) { + firstRef.current.style.flex = storedValue || defaultFlexRef.current; } } }, diff --git a/packages/graphiql/__mocks__/@graphiql/react.tsx b/packages/graphiql/__mocks__/@graphiql/react.tsx index 775af04683e..4cfafe1d0ed 100644 --- a/packages/graphiql/__mocks__/@graphiql/react.tsx +++ b/packages/graphiql/__mocks__/@graphiql/react.tsx @@ -15,6 +15,7 @@ export { ChevronDownIcon, ChevronUpIcon, CopyIcon, + DocsIcon, Dropdown, EditorContext, EditorContextProvider, @@ -25,13 +26,17 @@ export { ExplorerContextProvider, HistoryContext, HistoryContextProvider, + HistoryIcon, ImagePreview, + KeyboardShortcutIcon, onHasCompletion, MergeIcon, PlayIcon, PrettifyIcon, + ReloadIcon, SchemaContext, SchemaContextProvider, + SettingsIcon, StopIcon, StorageContext, StorageContextProvider, diff --git a/packages/graphiql/cypress/integration/docs.spec.ts b/packages/graphiql/cypress/integration/docs.spec.ts index ad009ddfe46..e6f608154ab 100644 --- a/packages/graphiql/cypress/integration/docs.spec.ts +++ b/packages/graphiql/cypress/integration/docs.spec.ts @@ -5,7 +5,7 @@ describe('GraphiQL DocExplorer - button', () => { cy.visit(`/`); }); it('Toggles doc pane on', () => { - cy.get('.docExplorerShow').click(); + cy.get('.graphiql-sidebar button').eq(0).click(); cy.get('.doc-explorer').should('be.visible'); }); @@ -19,7 +19,7 @@ describe('GraphiQL DocExplorer - button', () => { describe('GraphiQL DocExplorer - search', () => { before(() => { cy.visit(`/`); - cy.get('.docExplorerShow').click(); + cy.get('.graphiql-sidebar button').eq(0).click(); }); it('Searches docs for values', () => { @@ -66,7 +66,7 @@ describe('GraphiQL DocExplorer - search', () => { it('Allows clearing the search', () => { cy.visit(`/`); - cy.get('.docExplorerShow').click(); + cy.get('.graphiql-sidebar button').eq(0).click(); cy.get('label.search-box input').type('test'); cy.get('.doc-category-item').should('have.length', 7); cy.get('.search-box-clear').click(); @@ -78,7 +78,7 @@ describe('GraphiQL DocExplorer - search', () => { describe('GraphQL DocExplorer - deprecated fields', () => { before(() => { cy.visit(`/`); - cy.get('.docExplorerShow').click(); + cy.get('.graphiql-sidebar button').eq(0).click(); }); it('should show deprecated fields category title', () => { cy.get('.doc-category>.doc-category-item').first().find('a').click(); diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 9650be6bc8d..b4822f85d2b 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -25,6 +25,7 @@ import { ChevronDownIcon, ChevronUpIcon, CopyIcon, + DocsIcon, EditorContextProvider, ExecuteButton, ExecutionContextProvider, @@ -32,11 +33,15 @@ import { ExplorerContextProvider, HeaderEditor, HistoryContextProvider, + HistoryIcon, + KeyboardShortcutIcon, MergeIcon, PrettifyIcon, QueryEditor, + ReloadIcon, ResponseEditor, SchemaContextProvider, + SettingsIcon, StorageContextProvider, ToolbarButton, UnStyledButton, @@ -64,7 +69,6 @@ import type { KeyMap, } from '@graphiql/react'; -import { ToolbarButton as LegacyToolbarButton } from './ToolbarButton'; import { ToolbarMenu, ToolbarMenuItem } from './ToolbarMenu'; import { DocExplorer } from './DocExplorer'; import { QueryHistory } from './QueryHistory'; @@ -535,11 +539,11 @@ const GraphiQLConsumeContexts = forwardRef< const prettify = usePrettifyEditors(); const docResize = useDragResize({ - defaultSizeRelation: 3, + defaultSizeRelation: 1 / 3, direction: 'horizontal', - initiallyHidden: explorerContext?.isVisible ? undefined : 'second', + initiallyHidden: explorerContext?.isVisible ? undefined : 'first', onHiddenElementChange: resizableElement => { - if (resizableElement === 'second') { + if (resizableElement === 'first') { explorerContext?.hide(); } else { explorerContext?.show(); @@ -679,110 +683,151 @@ class GraphiQLWithContext extends React.Component< return (
-
- {this.props.historyContext?.isVisible && ( -
- -
- )} -
-
- {this.props.beforeTopBarContent} -
- {logo} - this.props.historyContext?.toggle()} - title={ - this.props.historyContext?.isVisible - ? 'Hide History' - : 'Show History' +
+
+ {this.props.explorerContext ? ( + { + if (this.props.explorerContext?.isVisible) { + this.props.explorerContext?.hide(); + this.props.docResize.setHiddenElement('first'); + } else { + this.props.explorerContext?.show(); + this.props.docResize.setHiddenElement(null); } - label="History" - /> - this.props.schemaContext.introspect()} - title="Fetch GraphQL schema using introspection (Shift-Ctrl-R)" - label="Introspect" - /> -
- {this.props.explorerContext && - !this.props.explorerContext.isVisible && ( - - )} + }} + title={ + this.props.explorerContext.isVisible + ? 'Hide Documentation Explorer' + : 'Show Documentation Explorer' + }> + + + ) : null} + {this.props.historyContext ? ( + this.props.historyContext?.toggle()} + title={ + this.props.historyContext.isVisible + ? 'Hide History' + : 'Show History' + }> + + + ) : null} +
+
+ this.props.schemaContext.introspect()}> + + + + + + + + +
+
+
+
+
+ this.props.docResize.setHiddenElement('first')} + />
- {this.props.tabs ? ( - - {this.props.editorContext.tabs.map((tab, index) => ( - 1} - onSelect={() => { - this.props.executionContext.stop(); - this.props.editorContext.changeTab(index); - }} - onClose={() => { - if (this.props.editorContext.activeTabIndex === index) { - this.props.executionContext.stop(); +
+
+ {this.props.explorerContext?.isVisible ? ( +
+ ) : null} +
+
+ {this.props.historyContext?.isVisible && ( +
+ +
+ )} +
+
+ {this.props.beforeTopBarContent} +
{logo}
+
+ {this.props.tabs ? ( + + {this.props.editorContext.tabs.map((tab, index) => ( + 1} + onSelect={() => { + this.props.executionContext.stop(); + this.props.editorContext.changeTab(index); + }} + onClose={() => { + if (this.props.editorContext.activeTabIndex === index) { + this.props.executionContext.stop(); + } + this.props.editorContext.closeTab(index); + }} + tabProps={{ + 'aria-controls': 'graphiql-session', + id: `session-tab-${index}`, + }} + /> + ))} + { + this.props.editorContext.addTab(); }} /> - ))} - { - this.props.editorContext.addTab(); - }} - /> - - ) : null} -
-
-
-
-
-
- { - if ( - this.props.docResize.hiddenElement === 'second' - ) { - this.props.docResize.setHiddenElement(null); - } - }} - onCopyQuery={this.props.onCopyQuery} - onEdit={this.props.onEditQuery} - onEditOperationName={this.props.onEditOperationName} - readOnly={this.props.readOnly} - validationRules={this.props.validationRules} - /> + + ) : null} +
+
+
+
+
+
+ { + if ( + this.props.docResize.hiddenElement === 'first' + ) { + this.props.docResize.setHiddenElement(null); + } + }} + onCopyQuery={this.props.onCopyQuery} + onEdit={this.props.onEditQuery} + onEditOperationName={this.props.onEditOperationName} + readOnly={this.props.readOnly} + validationRules={this.props.validationRules} + /> +
{toolbar}
-
-
-
-
-
-
- { - if ( - this.props.editorToolsResize.hiddenElement === - 'second' - ) { - this.props.editorToolsResize.setHiddenElement( - null, - ); - } - this.setState({ - activeSecondaryEditor: 'variable', - }); - }}> - Variables - - {this.props.headerEditorEnabled ? ( + +
+
+
+
- Headers + Variables - ) : null} -
- { - if ( - this.props.editorToolsResize.hiddenElement === - 'second' - ) { - this.props.editorToolsResize.setHiddenElement(null); - } else { + {headerEditorEnabled ? ( + { + if ( + this.props.editorToolsResize.hiddenElement === + 'second' + ) { + this.props.editorToolsResize.setHiddenElement( + null, + ); + } + this.setState({ + activeSecondaryEditor: 'header', + }); + }}> + Headers + + ) : null} +
+ { this.props.editorToolsResize.setHiddenElement( - 'second', + this.props.editorToolsResize.hiddenElement === + 'second' + ? null + : 'second', ); - } - }}> - {this.props.editorToolsResize.hiddenElement === - 'second' ? ( - - ) : ( - - )} - + }}> + {this.props.editorToolsResize.hiddenElement === + 'second' ? ( + + ) : ( + + )} + +
-
-
-
- - {headerEditorEnabled && ( - +
+ - )} -
+ {headerEditorEnabled && ( + + )} +
+
-
-
-
-
-
-
- {this.props.executionContext.isFetching ? ( -
- ) : null} - - {footer} +
+
+
+
+
+ {this.props.executionContext.isFetching ? ( +
+ ) : null} + + {footer} +
-
-
-
-
-
- this.props.docResize.setHiddenElement('second')} - /> -
-
); } diff --git a/packages/graphiql/src/components/ToolbarButton.tsx b/packages/graphiql/src/components/ToolbarButton.tsx deleted file mode 100644 index 45d32da5671..00000000000 --- a/packages/graphiql/src/components/ToolbarButton.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2021 GraphQL Contributors. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React from 'react'; - -type ToolbarButtonProps = { - onClick: () => void; - title: string; - label: string; -}; - -type ToolbarButtonState = { - error: Error | null; -}; - -/** - * ToolbarButton - * - * A button to use within the Toolbar. - */ -export class ToolbarButton extends React.Component< - ToolbarButtonProps, - ToolbarButtonState -> { - constructor(props: ToolbarButtonProps) { - super(props); - this.state = { error: null }; - } - - render() { - const { error } = this.state; - return ( - - ); - } - - handleClick = () => { - try { - this.props.onClick(); - this.setState({ error: null }); - } catch (error) { - if (error instanceof Error) { - this.setState({ error }); - return; - } - throw error; - } - }; -} diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx index 82bba6cc2e4..d0959f015cb 100644 --- a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -542,29 +542,29 @@ describe('GraphiQL', () => { .spyOn(Element.prototype, 'getBoundingClientRect') .mockReturnValue({ left: 0, right: 1200 }); - const { container, getByLabelText } = render( - , - ); + const { container } = render(); - fireEvent.click(getByLabelText(/Open Documentation Explorer/i)); - const docExplorerResizer = container.querySelector( - '.docExplorerResizer', - ) as Element; + fireEvent.click( + container.querySelector('[title="Show Documentation Explorer"]'), + ); + const dragBar = container.querySelectorAll( + '.graphiql-horizontal-drag-bar', + )[0]; - fireEvent.mouseDown(docExplorerResizer, { + fireEvent.mouseDown(dragBar, { clientX: 3, }); - fireEvent.mouseMove(docExplorerResizer, { + fireEvent.mouseMove(dragBar, { buttons: 1, clientX: 800, }); - fireEvent.mouseUp(docExplorerResizer); + fireEvent.mouseUp(dragBar); // 797 / (1200 - 797) = 1.977667493796526 expect( - container.querySelector('.editorWrap').parentElement.style.flex, + container.querySelector('.docExplorerWrap').parentElement.style.flex, ).toBe('1.977667493796526'); clientWidthSpy.mockRestore(); diff --git a/packages/graphiql/src/css/app.css b/packages/graphiql/src/css/app.css index d0fd9ff8815..fc351be8ab2 100644 --- a/packages/graphiql/src/css/app.css +++ b/packages/graphiql/src/css/app.css @@ -1,19 +1,3 @@ -.graphiql-container, -.graphiql-container button, -.graphiql-container input { - color: var(--color-neutral-100); - font-size: 14px; -} - -.graphiql-container { - display: flex; - flex-direction: row; - height: 100%; - margin: 0; - overflow: hidden; - width: 100%; -} - .graphiql-container .editorWrap { display: flex; flex-direction: column; @@ -102,14 +86,6 @@ z-index: 5; } -.graphiql-container .docExplorerResizer { - cursor: col-resize; - height: 100%; - position: absolute; - width: 10px; - z-index: 10; -} - .graphiql-container .docExplorerHide { cursor: pointer; font-size: 18px; diff --git a/packages/graphiql/src/index.tsx b/packages/graphiql/src/index.tsx index b559e0b5a30..b46f214e1b4 100644 --- a/packages/graphiql/src/index.tsx +++ b/packages/graphiql/src/index.tsx @@ -40,7 +40,6 @@ export { DocExplorer } from './components/DocExplorer'; * Toolbar */ export { ToolbarMenu, ToolbarMenuItem } from './components/ToolbarMenu'; -export { ToolbarButton } from './components/ToolbarButton'; export { ToolbarSelect, ToolbarSelectOption } from './components/ToolbarSelect'; /** diff --git a/packages/graphiql/src/style.css b/packages/graphiql/src/style.css index 839027ad6b8..a12eb4ec5e0 100644 --- a/packages/graphiql/src/style.css +++ b/packages/graphiql/src/style.css @@ -1,3 +1,44 @@ +/* Everything */ +.graphiql-container { + display: flex; + height: 100%; + margin: 0; + overflow: hidden; + width: 100%; +} + +/* The sidebar */ +.graphiql-container .graphiql-sidebar { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: var(--px-8); + width: var(--sidebar-width); +} +.graphiql-container .graphiql-sidebar button { + color: var(--color-neutral-60); + height: var(--sidebar-width); + width: var(--sidebar-width); +} +.graphiql-container .graphiql-sidebar button.active { + color: var(--color-neutral-100); +} +.graphiql-container .graphiql-sidebar button:not(:first-child) { + margin-top: var(--px-4); +} +.graphiql-container .graphiql-sidebar button > svg { + height: calc(var(--sidebar-width) - (2 * var(--px-12))); + padding: var(--px-12); + pointer-events: none; + width: calc(var(--sidebar-width) - (2 * var(--px-12))); +} + +/* The main content, i.e. everything except the sidebar */ +.graphiql-container .graphiql-main { + display: flex; + flex: 1; +} + /* The whole session, i.e. editors and response */ .graphiql-container .graphiql-session { background-color: var(--color-neutral-7); @@ -151,3 +192,16 @@ padding: var(--px-12); width: var(--px-12); } + +/* Generic spin animation */ +.graphiql-spin { + animation: spin 1s linear 0s infinite; +} +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +}