diff --git a/.changeset/olive-seals-shake.md b/.changeset/olive-seals-shake.md new file mode 100644 index 00000000000..340a7086723 --- /dev/null +++ b/.changeset/olive-seals-shake.md @@ -0,0 +1,6 @@ +--- +'graphiql': minor +'@graphiql/react': minor +--- + +Move the logic of the result viewer from the `graphiql` package into a hook `useResponseEditor` provided by `@graphiql/react` diff --git a/packages/graphiql/src/utility/__tests__/importCodeMirror.spec.ts b/packages/graphiql-react/src/editor/__tests__/common.spec.ts similarity index 88% rename from packages/graphiql/src/utility/__tests__/importCodeMirror.spec.ts rename to packages/graphiql-react/src/editor/__tests__/common.spec.ts index 8f0774a93f8..f811b62c4f0 100644 --- a/packages/graphiql/src/utility/__tests__/importCodeMirror.spec.ts +++ b/packages/graphiql-react/src/editor/__tests__/common.spec.ts @@ -1,4 +1,4 @@ -import { importCodeMirror } from '../importCodeMirror'; +import { importCodeMirror } from '../common'; describe('importCodeMirror', () => { it('should dynamically load codemirror module', async () => { diff --git a/packages/graphiql-react/src/editor/components/image-preview.tsx b/packages/graphiql-react/src/editor/components/image-preview.tsx new file mode 100644 index 00000000000..38e3fd5c40f --- /dev/null +++ b/packages/graphiql-react/src/editor/components/image-preview.tsx @@ -0,0 +1,88 @@ +import type { Token } from 'codemirror'; +import { useEffect, useRef, useState } from 'react'; + +type ImagePreviewProps = { token: Token }; + +type Dimensions = { + width: number | null; + height: number | null; +}; + +export function ImagePreview(props: ImagePreviewProps) { + const [dimensions, setDimensions] = useState({ + width: null, + height: null, + }); + const [mime, setMime] = useState(null); + + const ref = useRef(null); + + const src = tokenToURL(props.token)?.href; + + useEffect(() => { + if (!ref.current) { + return; + } + if (!src) { + setDimensions({ width: null, height: null }); + setMime(null); + return; + } + + fetch(src, { method: 'HEAD' }) + .then(response => { + setMime(response.headers.get('Content-Type')); + }) + .catch(() => { + setMime(null); + }); + }, [src]); + + const dims = + dimensions.width !== null && dimensions.height !== null ? ( +
+ {dimensions.width}x{dimensions.height} + {mime !== null ? ' ' + mime : null} +
+ ) : null; + + return ( +
+ { + setDimensions({ + width: ref.current?.naturalWidth ?? null, + height: ref.current?.naturalHeight ?? null, + }); + }} + ref={ref} + src={src} + /> + {dims} +
+ ); +} + +ImagePreview.shouldRender = function shouldRender(token: Token) { + const url = tokenToURL(token); + return url ? isImageURL(url) : false; +}; + +function tokenToURL(token: Token) { + if (token.type !== 'string') { + return; + } + + const value = token.string.slice(1).slice(0, -1).trim(); + + try { + const location = window.location; + return new URL(value, location.protocol + '//' + location.host); + } catch (err) { + return; + } +} + +function isImageURL(url: URL) { + return /(bmp|gif|jpeg|jpg|png|svg)$/.test(url.pathname); +} diff --git a/packages/graphiql-react/src/editor/components/index.ts b/packages/graphiql-react/src/editor/components/index.ts new file mode 100644 index 00000000000..ea99de1ccdf --- /dev/null +++ b/packages/graphiql-react/src/editor/components/index.ts @@ -0,0 +1,3 @@ +import { ImagePreview } from './image-preview'; + +export { ImagePreview }; diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index bfc151cd4c7..c58d2529653 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -5,18 +5,22 @@ import { CodeMirrorEditor } from './types'; export type EditorContextType = { headerEditor: CodeMirrorEditor | null; queryEditor: CodeMirrorEditor | null; + responseEditor: CodeMirrorEditor | null; variableEditor: CodeMirrorEditor | null; setHeaderEditor(newEditor: CodeMirrorEditor): void; setQueryEditor(newEditor: CodeMirrorEditor): void; + setResponseEditor(newEditor: CodeMirrorEditor): void; setVariableEditor(newEditor: CodeMirrorEditor): void; }; export const EditorContext = createContext({ headerEditor: null, queryEditor: null, + responseEditor: null, variableEditor: null, setHeaderEditor() {}, setQueryEditor() {}, + setResponseEditor() {}, setVariableEditor() {}, }); @@ -28,6 +32,9 @@ export function EditorContextProvider(props: { null, ); const [queryEditor, setQueryEditor] = useState(null); + const [responseEditor, setResponseEditor] = useState( + null, + ); const [variableEditor, setVariableEditor] = useState( null, ); @@ -36,9 +43,11 @@ export function EditorContextProvider(props: { value={{ headerEditor, queryEditor, + responseEditor, variableEditor, setHeaderEditor, setQueryEditor, + setResponseEditor, setVariableEditor, }}> {props.children} diff --git a/packages/graphiql-react/src/editor/index.ts b/packages/graphiql-react/src/editor/index.ts index e1265adae0f..f891a50339d 100644 --- a/packages/graphiql-react/src/editor/index.ts +++ b/packages/graphiql-react/src/editor/index.ts @@ -1,26 +1,36 @@ import { onHasCompletion } from './completion'; +import { ImagePreview } from './components'; import { EditorContext, EditorContextProvider } from './context'; import { useHeaderEditor } from './header-editor'; import { useQueryEditor } from './query-editor'; +import { useResponseEditor } from './response-editor'; import { useVariableEditor } from './variable-editor'; import type { EditorContextType } from './context'; import type { UseHeaderEditorArgs } from './header-editor'; import type { UseQueryEditorArgs } from './query-editor'; +import type { + ResponseTooltipType, + UseResponseEditorArgs, +} from './response-editor'; import type { UseVariableEditorArgs } from './variable-editor'; export { onHasCompletion, + ImagePreview, EditorContext, EditorContextProvider, useHeaderEditor, useQueryEditor, + useResponseEditor, useVariableEditor, }; export type { EditorContextType, + ResponseTooltipType, UseHeaderEditorArgs, UseQueryEditorArgs, + UseResponseEditorArgs, UseVariableEditorArgs, }; diff --git a/packages/graphiql-react/src/editor/response-editor.tsx b/packages/graphiql-react/src/editor/response-editor.tsx new file mode 100644 index 00000000000..feaa654fa40 --- /dev/null +++ b/packages/graphiql-react/src/editor/response-editor.tsx @@ -0,0 +1,123 @@ +import type { Position, Token } from 'codemirror'; +import { ComponentType, useContext, useEffect, useRef } from 'react'; +import ReactDOM from 'react-dom'; + +import { commonKeys, importCodeMirror } from './common'; +import { ImagePreview } from './components'; +import { EditorContext } from './context'; +import { useResizeEditor, useSynchronizeValue } from './hooks'; +import { CodeMirrorEditor } from './types'; + +export type ResponseTooltipType = ComponentType<{ pos: Position }>; + +export type UseResponseEditorArgs = { + ResponseTooltip?: ResponseTooltipType; + editorTheme?: string; + value?: string; +}; + +export function useResponseEditor({ + ResponseTooltip, + editorTheme = 'graphiql', + value, +}: UseResponseEditorArgs = {}) { + const context = useContext(EditorContext); + const ref = useRef(null); + + const responseTooltipRef = useRef( + ResponseTooltip, + ); + useEffect(() => { + responseTooltipRef.current = ResponseTooltip; + }, [ResponseTooltip]); + + if (!context) { + throw new Error( + 'Tried to call the `useResponseEditor` hook without the necessary context. Make sure that the `EditorContextProvider` from `@graphiql/react` is rendered higher in the tree.', + ); + } + + const { responseEditor, setResponseEditor } = context; + + useEffect(() => { + let isActive = true; + importCodeMirror( + [ + import('codemirror/addon/fold/foldgutter'), + import('codemirror/addon/fold/brace-fold'), + import('codemirror/addon/dialog/dialog'), + import('codemirror/addon/search/search'), + import('codemirror/addon/search/searchcursor'), + import('codemirror/addon/search/jump-to-line'), + // @ts-expect-error + import('codemirror/keymap/sublime'), + import('codemirror-graphql/esm/results/mode'), + import('codemirror-graphql/esm/utils/info-addon'), + ], + { useCommonAddons: false }, + ).then(CodeMirror => { + // Don't continue if the effect has already been cleaned up + if (!isActive) { + return; + } + + // Handle image tooltips and custom tooltips + const tooltipDiv = document.createElement('div'); + CodeMirror.registerHelper( + 'info', + 'graphql-results', + (token: Token, _options: any, _cm: CodeMirrorEditor, pos: Position) => { + const infoElements: JSX.Element[] = []; + + const ResponseTooltipComponent = responseTooltipRef.current; + if (ResponseTooltipComponent) { + infoElements.push(); + } + + if (ImagePreview.shouldRender(token)) { + infoElements.push( + , + ); + } + + if (!infoElements.length) { + ReactDOM.unmountComponentAtNode(tooltipDiv); + return null; + } + ReactDOM.render(infoElements, tooltipDiv); + return tooltipDiv; + }, + ); + + const container = ref.current; + if (!container) { + return; + } + + const newEditor = CodeMirror(container, { + lineWrapping: true, + readOnly: true, + theme: editorTheme, + mode: 'graphql-results', + keyMap: 'sublime', + foldGutter: true, + gutters: ['CodeMirror-foldgutter'], + // @ts-expect-error + info: true, + extraKeys: commonKeys, + }); + + setResponseEditor(newEditor); + }); + + return () => { + isActive = false; + }; + }, [editorTheme, setResponseEditor]); + + useSynchronizeValue(responseEditor, value); + + useResizeEditor(responseEditor, ref); + + return ref; +} diff --git a/packages/graphiql-react/src/index.ts b/packages/graphiql-react/src/index.ts index 4924bdc40e0..2b46e62cf23 100644 --- a/packages/graphiql-react/src/index.ts +++ b/packages/graphiql-react/src/index.ts @@ -1,9 +1,11 @@ import { EditorContext, EditorContextProvider, + ImagePreview, onHasCompletion, useHeaderEditor, useQueryEditor, + useResponseEditor, useVariableEditor, } from './editor'; import { @@ -14,8 +16,10 @@ import { import type { EditorContextType, + ResponseTooltipType, UseHeaderEditorArgs, UseQueryEditorArgs, + UseResponseEditorArgs, UseVariableEditorArgs, } from './editor'; import type { @@ -29,9 +33,11 @@ export { // editor EditorContext, EditorContextProvider, + ImagePreview, onHasCompletion, useHeaderEditor, useQueryEditor, + useResponseEditor, useVariableEditor, // explorer ExplorerContext, @@ -42,8 +48,10 @@ export { export type { // editor EditorContextType, + ResponseTooltipType, UseHeaderEditorArgs, UseQueryEditorArgs, + UseResponseEditorArgs, UseVariableEditorArgs, // explorer ExplorerContextType, diff --git a/packages/graphiql/__mocks__/@graphiql/react.ts b/packages/graphiql/__mocks__/@graphiql/react.ts index cb8344cd073..aca30c3f5ad 100644 --- a/packages/graphiql/__mocks__/@graphiql/react.ts +++ b/packages/graphiql/__mocks__/@graphiql/react.ts @@ -6,6 +6,7 @@ import { useExplorerNavStack, useHeaderEditor as _useHeaderEditor, useQueryEditor as _useQueryEditor, + useResponseEditor as _useResponseEditor, useVariableEditor as _useVariableEditor, } from '@graphiql/react'; import type { @@ -139,6 +140,12 @@ export const useQueryEditor: typeof _useQueryEditor = function useQueryEditor({ return useMockedEditor('query', value, onEdit); }; +export const useResponseEditor: typeof _useResponseEditor = function useResponseEditor({ + value, +}) { + return useMockedEditor('query', value); +}; + export const useVariableEditor: typeof _useVariableEditor = function useVariableEditor({ onEdit, value, diff --git a/packages/graphiql/cypress/integration/init.spec.ts b/packages/graphiql/cypress/integration/init.spec.ts index f4802d7aca8..8e1cf4cf40c 100644 --- a/packages/graphiql/cypress/integration/init.spec.ts +++ b/packages/graphiql/cypress/integration/init.spec.ts @@ -48,11 +48,8 @@ describe('GraphiQL On Initialization', () => { it('Shows the expected error when the schema is invalid', () => { cy.visit(`/?bad=true`); cy.wait(200); - cy.window().then(w => { - // @ts-ignore - const value = w.g.resultComponent.viewer.getValue(); - // this message changes between graphql 15 & 16 - expect(value).to.contain('Names must'); + cy.get('section#graphiql-result-viewer').should(element => { + expect(element.get(0).innerText).to.contain('Names must'); }); }); }); diff --git a/packages/graphiql/cypress/support/commands.ts b/packages/graphiql/cypress/support/commands.ts index 30d7ca99d79..74bca825e4c 100644 --- a/packages/graphiql/cypress/support/commands.ts +++ b/packages/graphiql/cypress/support/commands.ts @@ -32,7 +32,6 @@ declare namespace Cypress { visitWithOp(op: Op): Chainable; clickPrettify(): Chainable; assertHasValues(op: Op): Chainable; - assertResult(result: MockResult): Chainable; assertQueryResult( op: Op, expectedResult: MockResult, @@ -90,19 +89,10 @@ Cypress.Commands.add('assertQueryResult', (op, mockSuccess, timeout = 200) => { cy.visitWithOp(op); cy.clickExecuteQuery(); cy.wait(timeout); - cy.window().then(w => { - // @ts-ignore - const value = w.g.resultComponent.viewer.getValue(); - expect(value).to.deep.equal(JSON.stringify(mockSuccess, null, 2)); - }); -}); - -Cypress.Commands.add('assertResult', (expectedResult, timeout = 200) => { - cy.wait(timeout); - cy.window().then(w => { - // @ts-ignore - const value = w.g.resultComponent.viewer.getValue(); - expect(value).to.deep.equal(JSON.stringify(expectedResult, null, 2)); + cy.get('section#graphiql-result-viewer').should(element => { + // Replace "invisible" whitespace characters with regular whitespace + const response = element.get(0).innerText.replace(/[\u00a0]/g, ' '); + expect(response).to.equal(JSON.stringify(mockSuccess, null, 2)); }); }); diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index b7817c150d8..ee575e7675f 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -9,8 +9,6 @@ import React, { ComponentType, PropsWithChildren, MouseEventHandler, - Component, - FunctionComponent, ReactNode, } from 'react'; import { @@ -46,20 +44,19 @@ import type { EditorContextType, ExplorerContextType, ExplorerFieldDef, + ResponseTooltipType, } from '@graphiql/react'; import { ExecuteButton } from './ExecuteButton'; -import { ImagePreview } from './ImagePreview'; import { ToolbarButton } from './ToolbarButton'; import { ToolbarGroup } from './ToolbarGroup'; import { ToolbarMenu, ToolbarMenuItem } from './ToolbarMenu'; import { QueryEditor } from './QueryEditor'; import { VariableEditor } from './VariableEditor'; import { HeaderEditor } from './HeaderEditor'; -import { ResultViewer } from './ResultViewer'; +import { ResultViewer, RESULT_VIEWER_ID } from './ResultViewer'; import { DocExplorer } from './DocExplorer'; import { QueryHistory } from './QueryHistory'; -import CodeMirrorSizer from '../utility/CodeMirrorSizer'; import StorageAPI, { Storage } from '../utility/StorageAPI'; import getSelectedOperationName from '../utility/getSelectedOperationName'; import debounce from '../utility/debounce'; @@ -250,7 +247,7 @@ export type GraphiQLProps = { /** * Custom results tooltip component */ - ResultsTooltip?: typeof Component | FunctionComponent; + ResultsTooltip?: ResponseTooltipType; /** * decide whether schema responses should be validated. * @@ -447,17 +444,14 @@ class GraphiQLWithContext extends React.Component< _introspectionQueryName: string; _introspectionQuerySansSubscriptions: string; - codeMirrorSizer!: CodeMirrorSizer; // Ensure the component is mounted to execute async setState componentIsMounted: boolean; // refs graphiqlContainer: Maybe; - resultComponent: Maybe; _queryHistory: Maybe; _historyStore: Maybe; editorBarComponent: Maybe; - resultViewerElement: Maybe; constructor(props: GraphiQLWithContextProps) { super(props); @@ -673,9 +667,6 @@ class GraphiQLWithContext extends React.Component< this.fetchSchema(); } - // Utility for keeping CodeMirror correctly sized. - this.codeMirrorSizer = new CodeMirrorSizer(); - if (typeof window !== 'undefined') { window.g = this; } @@ -759,12 +750,6 @@ class GraphiQLWithContext extends React.Component< ); } - componentDidUpdate() { - // If this update caused DOM nodes to have changed sizes, update the - // corresponding CodeMirror instance sizes to match. - this.codeMirrorSizer.updateSizes([this.resultComponent]); - } - // Use it when the state change is async // TODO: Annotate correctly this function safeSetState = (nextState: any, callback?: any): void => { @@ -1052,16 +1037,9 @@ class GraphiQLWithContext extends React.Component< )} { - this.resultViewerElement = n; - }} - ref={c => { - this.resultComponent = c; - }} value={this.state.response} editorTheme={this.props.editorTheme} - ResultsTooltip={this.props.ResultsTooltip} - ImagePreview={ImagePreview} + ResponseTooltip={this.props.ResultsTooltip} /> {footer} @@ -1126,9 +1104,7 @@ class GraphiQLWithContext extends React.Component< this.props.editorContext?.queryEditor?.refresh(); this.props.editorContext?.variableEditor?.refresh(); this.props.editorContext?.headerEditor?.refresh(); - if (this.resultComponent) { - this.resultComponent.getCodeMirror().refresh(); - } + this.props.editorContext?.responseEditor?.refresh(); } /** @@ -1936,20 +1912,17 @@ class GraphiQLWithContext extends React.Component< if (event.button !== 0 || event.ctrlKey) { return false; } - let target = event.target as Element; + const target = event.target; + if (!(target instanceof Element)) { + return false; + } // We use codemirror's gutter as the drag bar. if (target.className.indexOf('CodeMirror-gutter') !== 0) { return false; } // Specifically the result window's drag bar. - const resultWindow = this.resultViewerElement; - while (target) { - if (target === resultWindow) { - return true; - } - target = target.parentNode as Element; - } - return false; + const resultWindow = target.closest('section'); + return resultWindow ? resultWindow.id === RESULT_VIEWER_ID : false; } private handleDocsResizeStart: MouseEventHandler< diff --git a/packages/graphiql/src/components/ImagePreview.tsx b/packages/graphiql/src/components/ImagePreview.tsx deleted file mode 100644 index c55042ae34b..00000000000 --- a/packages/graphiql/src/components/ImagePreview.tsx +++ /dev/null @@ -1,113 +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'; - -function tokenToURL(token: any) { - if (token.type !== 'string') { - return; - } - - const value = token.string.slice(1).slice(0, -1).trim(); - - try { - const location = window.location; - return new URL(value, location.protocol + '//' + location.host); - } catch (err) { - return; - } -} - -function isImageURL(url: URL) { - return /(bmp|gif|jpeg|jpg|png|svg)$/.test(url.pathname); -} - -type ImagePreviewProps = { - token: any; -}; - -type ImagePreviewState = { - width: number | null; - height: number | null; - src: string | null; - mime: string | null; -}; - -export class ImagePreview extends React.Component< - ImagePreviewProps, - ImagePreviewState -> { - _node: HTMLImageElement | null = null; - - static shouldRender(token: any) { - const url = tokenToURL(token); - return url ? isImageURL(url) : false; - } - - state = { - width: null, - height: null, - src: null, - mime: null, - }; - - componentDidMount() { - this._updateMetadata(); - } - - componentDidUpdate() { - this._updateMetadata(); - } - - render() { - let dims = null; - if (this.state.width !== null && this.state.height !== null) { - let dimensions = this.state.width + 'x' + this.state.height; - if (this.state.mime !== null) { - dimensions += ' ' + this.state.mime; - } - - dims =
{dimensions}
; - } - - return ( -
- this._updateMetadata()} - ref={node => { - this._node = node; - }} - src={tokenToURL(this.props.token)?.href} - /> - {dims} -
- ); - } - - _updateMetadata() { - if (!this._node) { - return; - } - - const width = this._node.naturalWidth; - const height = this._node.naturalHeight; - const src = this._node.src; - - if (src !== this.state.src) { - this.setState({ src }); - fetch(src, { method: 'HEAD' }).then(response => { - this.setState({ - mime: response.headers.get('Content-Type'), - }); - }); - } - - if (width !== this.state.width || height !== this.state.height) { - this.setState({ height, width }); - } - } -} diff --git a/packages/graphiql/src/components/ResultViewer.tsx b/packages/graphiql/src/components/ResultViewer.tsx index c302a9c64e8..60fdbd9ad69 100644 --- a/packages/graphiql/src/components/ResultViewer.tsx +++ b/packages/graphiql/src/components/ResultViewer.tsx @@ -5,153 +5,27 @@ * LICENSE file in the root directory of this source tree. */ -import React, { Component, FunctionComponent } from 'react'; -import type * as CM from 'codemirror'; -import ReactDOM from 'react-dom'; -import commonKeys from '../utility/commonKeys'; -import { SizerComponent } from '../utility/CodeMirrorSizer'; -import { ImagePreview as ImagePreviewComponent } from './ImagePreview'; -import { importCodeMirror } from '../utility/importCodeMirror'; -import { CodeMirrorEditor } from '../types'; +import { useResponseEditor, UseResponseEditorArgs } from '@graphiql/react'; +import React from 'react'; -type ResultViewerProps = { - value?: string; - editorTheme?: string; - ResultsTooltip?: typeof Component | FunctionComponent; - ImagePreview: typeof ImagePreviewComponent; - registerRef: (node: HTMLElement) => void; -}; +export const RESULT_VIEWER_ID = 'graphiql-result-viewer'; /** * ResultViewer * * Maintains an instance of CodeMirror for viewing a GraphQL response. * - * Props: - * - * - value: The text of the editor. - * */ -export class ResultViewer extends React.Component - implements SizerComponent { - viewer: CodeMirrorEditor | null = null; - _node: HTMLElement | null = null; - - componentDidMount() { - this.initializeEditor(); - } - - shouldComponentUpdate(nextProps: ResultViewerProps) { - return this.props.value !== nextProps.value; - } - - componentDidUpdate() { - if (this.viewer) { - this.viewer.setValue(this.props.value || ''); - } - } - - componentWillUnmount() { - this.viewer = null; - } - - render() { - return ( -
{ - if (node) { - this.props.registerRef(node); - this._node = node; - } - }} - /> - ); - } - - allAddons = () => [ - import('codemirror/addon/fold/foldgutter'), - import('codemirror/addon/fold/brace-fold'), - import('codemirror/addon/dialog/dialog'), - import('codemirror/addon/search/search'), - import('codemirror/addon/search/searchcursor'), - import('codemirror/addon/search/jump-to-line'), - // @ts-expect-error - import('codemirror/keymap/sublime'), - import('codemirror-graphql/results/mode'), - ]; - - async initializeEditor() { - // Lazily require to ensure requiring GraphiQL outside of a Browser context - // does not produce an error. - const CodeMirror = await importCodeMirror(this.allAddons(), { - useCommonAddons: false, - }); - const Tooltip = this.props.ResultsTooltip; - const ImagePreview = this.props.ImagePreview; - - if (Tooltip || ImagePreview) { - await import('codemirror-graphql/utils/info-addon'); - const tooltipDiv = document.createElement('div'); - CodeMirror.registerHelper( - 'info', - 'graphql-results', - (token: any, _options: any, _cm: CodeMirror.Editor, pos: any) => { - const infoElements: JSX.Element[] = []; - if (Tooltip) { - infoElements.push(); - } - - if ( - ImagePreview && - typeof ImagePreview.shouldRender === 'function' && - ImagePreview.shouldRender(token) - ) { - infoElements.push(); - } - - if (!infoElements.length) { - ReactDOM.unmountComponentAtNode(tooltipDiv); - return null; - } - ReactDOM.render(
{infoElements}
, tooltipDiv); - return tooltipDiv; - }, - ); - } - - this.viewer = CodeMirror(this._node!, { - lineWrapping: true, - value: this.props.value || '', - readOnly: true, - theme: this.props.editorTheme || 'graphiql', - mode: 'graphql-results', - keyMap: 'sublime', - foldGutter: { - // @ts-expect-error - minFoldSize: 4, - }, - gutters: ['CodeMirror-foldgutter'], - info: Boolean(this.props.ResultsTooltip || this.props.ImagePreview), - extraKeys: commonKeys, - }) as CodeMirrorEditor; - } - - /** - * Public API for retrieving the CodeMirror instance from this - * React component. - */ - getCodeMirror() { - return this.viewer as CM.Editor; - } - - /** - * Public API for retrieving the DOM client height for this component. - */ - getClientHeight() { - return this._node && this._node.clientHeight; - } +export function ResultViewer(props: UseResponseEditorArgs) { + const ref = useResponseEditor(props); + return ( +
+ ); } diff --git a/packages/graphiql/src/types.ts b/packages/graphiql/src/types.ts index d3a79c92a9e..ea4cc282e52 100644 --- a/packages/graphiql/src/types.ts +++ b/packages/graphiql/src/types.ts @@ -1,5 +1,3 @@ -import CM from 'codemirror'; - export type Maybe = T | null | undefined; export type ReactComponentLike = @@ -22,5 +20,3 @@ export type ReactNodeLike = | boolean | null | undefined; - -export type CodeMirrorEditor = CM.Editor & { options?: any }; diff --git a/packages/graphiql/src/utility/CodeMirrorSizer.ts b/packages/graphiql/src/utility/CodeMirrorSizer.ts deleted file mode 100644 index 9dbee004ed0..00000000000 --- a/packages/graphiql/src/utility/CodeMirrorSizer.ts +++ /dev/null @@ -1,37 +0,0 @@ -import CodeMirror from 'codemirror'; -import { Maybe } from '../types'; - -export interface SizerComponent { - getClientHeight: () => number | null; - getCodeMirror: () => CodeMirror.Editor; -} - -/** - * 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. - */ - -/** - * When a containing DOM node's height has been altered, trigger a resize of - * the related CodeMirror instance so that it is always correctly sized. - */ -export default class CodeMirrorSizer { - public sizes: Array = []; - - public updateSizes(components: Array>) { - components.forEach((component, i) => { - if (component) { - const size = component.getClientHeight(); - if (i <= this.sizes.length && size !== this.sizes[i]) { - const editor = component.getCodeMirror(); - if (editor) { - editor.setSize(null, null); // TODO: added the args here. double check no effects. might be version issue - } - } - this.sizes[i] = size; - } - }); - } -} diff --git a/packages/graphiql/src/utility/commonKeys.ts b/packages/graphiql/src/utility/commonKeys.ts deleted file mode 100644 index 4b2faed2c1e..00000000000 --- a/packages/graphiql/src/utility/commonKeys.ts +++ /dev/null @@ -1,20 +0,0 @@ -let isMacOs = false; - -if (typeof window === 'object') { - isMacOs = window.navigator.platform === 'MacIntel'; -} - -const commonKeys = { - // Persistent search box in Query Editor - [isMacOs ? 'Cmd-F' : 'Ctrl-F']: 'findPersistent', - 'Cmd-G': 'findPersistent', - 'Ctrl-G': 'findPersistent', - - // Editor improvements - 'Ctrl-Left': 'goSubwordLeft', - 'Ctrl-Right': 'goSubwordRight', - 'Alt-Left': 'goGroupLeft', - 'Alt-Right': 'goGroupRight', -}; - -export default commonKeys; diff --git a/packages/graphiql/src/utility/importCodeMirror.ts b/packages/graphiql/src/utility/importCodeMirror.ts deleted file mode 100644 index 3ba657ff608..00000000000 --- a/packages/graphiql/src/utility/importCodeMirror.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const commonCodeMirrorAddons = [ - import('codemirror/addon/hint/show-hint'), - import('codemirror/addon/edit/matchbrackets'), - import('codemirror/addon/edit/closebrackets'), - import('codemirror/addon/fold/brace-fold'), - import('codemirror/addon/fold/foldgutter'), - import('codemirror/addon/lint/lint'), - import('codemirror/addon/search/searchcursor'), - import('codemirror/addon/search/jump-to-line'), - import('codemirror/addon/dialog/dialog'), - // @ts-expect-error - import('codemirror/keymap/sublime'), -]; - -/** - * Dynamically import codemirror and dependencies - * This works for codemirror 5, not sure if the same imports work for 6 - */ -export async function importCodeMirror( - addons: Promise[], - options?: { useCommonAddons?: boolean }, -) { - const CodeMirror = await import('codemirror').then(c => - // Depending on bundler and settings the dynamic import either returns a - // function (e.g. parcel) or an object containing a `default` property - typeof c === 'function' ? c : c.default, - ); - const allAddons = - options?.useCommonAddons === false - ? addons - : commonCodeMirrorAddons.concat(addons); - await Promise.all(allAddons.map(addon => addon)); - return CodeMirror; -}