diff --git a/.changeset/weak-dancers-jog.md b/.changeset/weak-dancers-jog.md new file mode 100644 index 00000000000..a95c2bc5590 --- /dev/null +++ b/.changeset/weak-dancers-jog.md @@ -0,0 +1,79 @@ +--- +'graphiql': major +--- + +Remove `toolbar.additionalContent` and `toolbar.additionalComponent` props in favor of `GraphiQL.Toolbar` render props. + +## Migration from `toolbar.additionalContent` + +#### Before + +```jsx +My button }} /> +``` + +#### After + +```jsx + + + {({ merge, prettify, copy }) => ( + <> + {prettify} + {merge} + {copy} + + + )} + + +``` + +### Migration from `toolbar.additionalComponent` + +#### Before + +```jsx +My button; + }, + }} +/> +``` + +#### After + +```jsx + + + {({ merge, prettify, copy }) => ( + <> + {prettify} + {merge} + {copy} + + + )} + + +``` + +--- + +Additionally, you can sort default toolbar buttons in different order or remove unneeded buttons for you: + +```jsx + + + {({ prettify, copy }) => ( + <> + {copy /* Copy button will be first instead of default last */} + {/* Merge button is removed from toolbar */} + {prettify} + + )} + + +``` diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 19320b8c3ea..e9f422a5c4f 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -6,7 +6,6 @@ */ import React, { - ComponentType, Fragment, MouseEventHandler, PropsWithChildren, @@ -16,6 +15,10 @@ import React, { useState, useEffect, useMemo, + version, + Children, + JSX, + cloneElement, } from 'react'; import { @@ -61,7 +64,7 @@ import { WriteableEditorProps, } from '@graphiql/react'; -const majorVersion = parseInt(React.version.slice(0, 2), 10); +const majorVersion = parseInt(version.slice(0, 2), 10); if (majorVersion < 16) { throw new Error( @@ -73,20 +76,6 @@ if (majorVersion < 16) { ); } -export type GraphiQLToolbarConfig = { - /** - * This content will be rendered after the built-in buttons of the toolbar. - * Note that this will not apply if you provide a completely custom toolbar - * (by passing `GraphiQL.Toolbar` as child to the `GraphiQL` component). - */ - additionalContent?: React.ReactNode; - - /** - * same as above, except a component with access to context - */ - additionalComponent?: React.JSXElementConstructor; -}; - /** * API docs for this live here: * @@ -101,7 +90,6 @@ export type GraphiQLProps = Omit & * * @see https://github.com/graphql/graphiql#usage */ - export function GraphiQL({ dangerouslyAssumeSchemaIsValid, defaultQuery, @@ -137,7 +125,18 @@ export function GraphiQL({ 'The `GraphiQL` component requires a `fetcher` function to be passed as prop.', ); } - + // @ts-expect-error -- Prop is removed + if (props.toolbar?.additionalContent) { + throw new TypeError( + '`toolbar.additionalContent` was removed. Use render props on `GraphiQL.Toolbar` component instead.', + ); + } + // @ts-expect-error -- Prop is removed + if (props.toolbar?.additionalComponent) { + throw new TypeError( + '`toolbar.additionalComponent` was removed. Use render props on `GraphiQL.Toolbar` component instead.', + ); + } return ( { @@ -323,37 +312,35 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { 'success' | 'error' | null >(null); - const children = React.Children.toArray(props.children); - - const logo = children.find(child => - isChildComponentType(child, GraphiQL.Logo), - ) || ; - - const toolbar = children.find(child => - isChildComponentType(child, GraphiQL.Toolbar), - ) || ( - <> - - - - - - - {props.toolbar?.additionalContent} - {props.toolbar?.additionalComponent && ( - - )} - - ); - - const footer = children.find(child => - isChildComponentType(child, GraphiQL.Footer), + const { + logo = , + // @ts-expect-error -- Prop exists but hidden for users + toolbar = , + footer, + } = useMemo( + () => + Children.toArray(props.children).reduce<{ + logo?: ReactNode; + toolbar?: ReactNode; + footer?: ReactNode; + }>((acc, curr) => { + switch (getChildComponentType(curr)) { + case GraphiQL.Logo: + acc.logo = curr; + break; + case GraphiQL.Toolbar: + // @ts-expect-error -- fix type error + acc.toolbar = cloneElement(curr, { + onCopyQuery: props.onCopyQuery, + }); + break; + case GraphiQL.Footer: + acc.footer = curr; + break; + } + return acc; + }, {}), + [props.children, props.onCopyQuery], ); const onClickReference = useCallback(() => { @@ -927,53 +914,94 @@ function ShortKeys({ keyMap }: { keyMap: string }): ReactElement { } // Configure the UI by providing this Component as a child of GraphiQL. -function GraphiQLLogo(props: PropsWithChildren) { - return ( -
- {props.children || ( - - Graph - i - QL - - )} -
- ); +function GraphiQLLogo({ + children = ( + + Graph + i + QL + + ), +}: PropsWithChildren) { + return
{children}
; } -GraphiQLLogo.displayName = 'GraphiQLLogo'; +type ToolbarRenderProps = (props: { + prettify: ReactNode; + copy: ReactNode; + merge: ReactNode; +}) => JSX.Element; + +const DefaultToolbarRenderProps: ToolbarRenderProps = ({ + prettify, + copy, + merge, +}) => ( + <> + {prettify} + {merge} + {copy} + +); // Configure the UI by providing this Component as a child of GraphiQL. -function GraphiQLToolbar(props: PropsWithChildren) { - // eslint-disable-next-line react/jsx-no-useless-fragment - return <>{props.children}; -} +function GraphiQLToolbar({ + children = DefaultToolbarRenderProps, + // @ts-expect-error -- Hide this prop for user, we use cloneElement to pass onCopyQuery + onCopyQuery, +}: { + children?: ToolbarRenderProps; +}) { + if (typeof children !== 'function') { + throw new TypeError( + 'The `GraphiQL.Toolbar` component requires a render prop function as its child.', + ); + } + const onCopy = useCopyQuery({ onCopyQuery }); + const onMerge = useMergeQuery(); + const onPrettify = usePrettifyEditors(); + + const prettify = ( + + + ); -GraphiQLToolbar.displayName = 'GraphiQLToolbar'; + const merge = ( + + + ); + + const copy = ( + + + ); + + return children({ prettify, copy, merge }); +} // Configure the UI by providing this Component as a child of GraphiQL. function GraphiQLFooter(props: PropsWithChildren) { return
{props.children}
; } -GraphiQLFooter.displayName = 'GraphiQLFooter'; - -// Determines if the React child is of the same type of the provided React component -function isChildComponentType( - child: any, - component: T, -): child is T { +function getChildComponentType(child: ReactNode) { if ( - child?.type?.displayName && - child.type.displayName === component.displayName + child && + typeof child === 'object' && + 'type' in child && + typeof child.type === 'function' ) { - return true; + return child.type; } - - return child.type === component; } diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx index e278a1310fe..84571aaf663 100644 --- a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -599,28 +599,6 @@ describe('GraphiQL', () => { expect(getByText('My Exported Type Logo')).toBeInTheDocument(); }); }); - - it('can be overridden using a named component', async () => { - const WrappedLogo = () => { - return ( -
- My Named Component Logo -
- ); - }; - WrappedLogo.displayName = 'GraphiQLLogo'; - - const { container, getByText } = render( - - - , - ); - - await waitFor(() => { - expect(container.querySelector('.test-wrapper')).toBeInTheDocument(); - expect(getByText('My Named Component Logo')).toBeInTheDocument(); - }); - }); }); describe('GraphiQL.Toolbar', () => { @@ -628,7 +606,7 @@ describe('GraphiQL', () => { const { container } = render( - + {() => } , ); @@ -641,35 +619,6 @@ describe('GraphiQL', () => { ).toHaveLength(1); }); }); - - it('can be overridden using a named component', async () => { - const WrappedToolbar = () => { - return ( -
- - - - , -
- ); - }; - WrappedToolbar.displayName = 'GraphiQLToolbar'; - - const { container } = render( - - - , - ); - - await waitFor(() => { - expect(container.querySelector('.test-wrapper')).toBeInTheDocument(); - expect( - container.querySelectorAll( - '[role="toolbar"] .graphiql-toolbar-button', - ), - ).toHaveLength(1); - }); - }); }); describe('GraphiQL.Footer', () => { @@ -688,33 +637,6 @@ describe('GraphiQL', () => { ).toHaveLength(1); }); }); - - it('can be overridden using a named component', async () => { - const WrappedFooter = () => { - return ( -
- - - - , -
- ); - }; - WrappedFooter.displayName = 'GraphiQLFooter'; - - const { container } = render( - - - , - ); - - await waitFor(() => { - expect(container.querySelector('.test-wrapper')).toBeInTheDocument(); - expect( - container.querySelectorAll('.graphiql-footer button'), - ).toHaveLength(1); - }); - }); }); }); });