Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v4] Remove toolbar.additionalContent and toolbar.additionalComponent props in favor of GraphiQL.Toolbar render props. #3707

Merged
merged 4 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .changeset/weak-dancers-jog.md
Original file line number Diff line number Diff line change
@@ -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
<GraphiQL toolbar={{ additionalContent: <button>My button</button> }} />
```

#### After

```jsx
<GraphiQL>
<GraphiQL.Toolbar>
{({ merge, prettify, copy }) => (
<>
{prettify}
{merge}
{copy}
<button>My button</button>
</>
)}
</GraphiQL.Toolbar>
</GraphiQL>
```

### Migration from `toolbar.additionalComponent`

#### Before

```jsx
<GraphiQL
toolbar={{
additionalComponent: function MyComponentWithAccessToContext() {
return <button>My button</button>;
},
}}
/>
```

#### After

```jsx
<GraphiQL>
<GraphiQL.Toolbar>
{({ merge, prettify, copy }) => (
<>
{prettify}
{merge}
{copy}
<MyComponentWithAccessToContext />
</>
)}
</GraphiQL.Toolbar>
</GraphiQL>
```

---

Additionally, you can sort default toolbar buttons in different order or remove unneeded buttons for you:

```jsx
<GraphiQL>
<GraphiQL.Toolbar>
{({ prettify, copy }) => (
<>
{copy /* Copy button will be first instead of default last */}
{/* Merge button is removed from toolbar */}
{prettify}
</>
)}
</GraphiQL.Toolbar>
</GraphiQL>
```
dimaMachina marked this conversation as resolved.
Show resolved Hide resolved
216 changes: 122 additions & 94 deletions packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

import React, {
ComponentType,
Fragment,
MouseEventHandler,
PropsWithChildren,
Expand All @@ -16,6 +15,10 @@
useState,
useEffect,
useMemo,
version,
Children,
JSX,
cloneElement,
} from 'react';

import {
Expand Down Expand Up @@ -61,7 +64,7 @@
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(
Expand All @@ -73,20 +76,6 @@
);
}

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<any>;
};

/**
* API docs for this live here:
*
Expand All @@ -101,7 +90,6 @@
*
* @see https://github.com/graphql/graphiql#usage
*/

export function GraphiQL({
dangerouslyAssumeSchemaIsValid,
defaultQuery,
Expand Down Expand Up @@ -137,7 +125,18 @@
'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(

Check warning on line 130 in packages/graphiql/src/components/GraphiQL.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql/src/components/GraphiQL.tsx#L130

Added line #L130 was not covered by tests
'`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(

Check warning on line 136 in packages/graphiql/src/components/GraphiQL.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql/src/components/GraphiQL.tsx#L136

Added line #L136 was not covered by tests
'`toolbar.additionalComponent` was removed. Use render props on `GraphiQL.Toolbar` component instead.',
);
}
return (
<GraphiQLProvider
getDefaultFieldNames={getDefaultFieldNames}
Expand Down Expand Up @@ -207,11 +206,6 @@
* @default true
*/
isHeadersEditorEnabled?: boolean;
/**
* An object that allows configuration of the toolbar next to the query
* editor.
*/
toolbar?: GraphiQLToolbarConfig;
/**
* Indicates if settings for persisting headers should appear in the
* settings modal.
Expand Down Expand Up @@ -245,11 +239,6 @@
: undefined,
[props.forcedTheme],
);

const copy = useCopyQuery({ onCopyQuery: props.onCopyQuery });
const merge = useMergeQuery();
const prettify = usePrettifyEditors();

const { theme, setTheme } = useTheme();

useEffect(() => {
Expand Down Expand Up @@ -323,37 +312,35 @@
'success' | 'error' | null
>(null);

const children = React.Children.toArray(props.children);

const logo = children.find(child =>
isChildComponentType(child, GraphiQL.Logo),
) || <GraphiQL.Logo />;

const toolbar = children.find(child =>
isChildComponentType(child, GraphiQL.Toolbar),
) || (
<>
<ToolbarButton onClick={prettify} label="Prettify query (Shift-Ctrl-P)">
<PrettifyIcon className="graphiql-toolbar-icon" aria-hidden="true" />
</ToolbarButton>
<ToolbarButton
onClick={merge}
label="Merge fragments into query (Shift-Ctrl-M)"
>
<MergeIcon className="graphiql-toolbar-icon" aria-hidden="true" />
</ToolbarButton>
<ToolbarButton onClick={copy} label="Copy query (Shift-Ctrl-C)">
<CopyIcon className="graphiql-toolbar-icon" aria-hidden="true" />
</ToolbarButton>
{props.toolbar?.additionalContent}
{props.toolbar?.additionalComponent && (
<props.toolbar.additionalComponent />
)}
</>
);

const footer = children.find(child =>
isChildComponentType(child, GraphiQL.Footer),
const {
logo = <GraphiQL.Logo />,
// @ts-expect-error -- Prop exists but hidden for users
toolbar = <GraphiQL.Toolbar onCopyQuery={props.onCopyQuery} />,
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(() => {
Expand Down Expand Up @@ -927,53 +914,94 @@
}

// Configure the UI by providing this Component as a child of GraphiQL.
function GraphiQLLogo<TProps>(props: PropsWithChildren<TProps>) {
return (
<div className="graphiql-logo">
{props.children || (
<a
className="graphiql-logo-link"
href="https://github.com/graphql/graphiql"
target="_blank"
rel="noreferrer"
>
Graph
<em>i</em>
QL
</a>
)}
</div>
);
function GraphiQLLogo<TProps>({
children = (
<a
className="graphiql-logo-link"
href="https://github.com/graphql/graphiql"
target="_blank"
rel="noreferrer"
>
Graph
<em>i</em>
QL
</a>
),
}: PropsWithChildren<TProps>) {
return <div className="graphiql-logo">{children}</div>;
}

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<TProps>(props: PropsWithChildren<TProps>) {
// 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(

Check warning on line 961 in packages/graphiql/src/components/GraphiQL.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql/src/components/GraphiQL.tsx#L961

Added line #L961 was not covered by tests
'The `GraphiQL.Toolbar` component requires a render prop function as its child.',
);
}
const onCopy = useCopyQuery({ onCopyQuery });
const onMerge = useMergeQuery();
const onPrettify = usePrettifyEditors();

const prettify = (
<ToolbarButton onClick={onPrettify} label="Prettify query (Shift-Ctrl-P)">
<PrettifyIcon className="graphiql-toolbar-icon" aria-hidden="true" />
</ToolbarButton>
);

GraphiQLToolbar.displayName = 'GraphiQLToolbar';
const merge = (
<ToolbarButton
onClick={onMerge}
label="Merge fragments into query (Shift-Ctrl-M)"
>
<MergeIcon className="graphiql-toolbar-icon" aria-hidden="true" />
</ToolbarButton>
);

const copy = (
<ToolbarButton onClick={onCopy} label="Copy query (Shift-Ctrl-C)">
<CopyIcon className="graphiql-toolbar-icon" aria-hidden="true" />
</ToolbarButton>
);

return children({ prettify, copy, merge });
}

// Configure the UI by providing this Component as a child of GraphiQL.
function GraphiQLFooter<TProps>(props: PropsWithChildren<TProps>) {
return <div className="graphiql-footer">{props.children}</div>;
}

GraphiQLFooter.displayName = 'GraphiQLFooter';

// Determines if the React child is of the same type of the provided React component
function isChildComponentType<T extends ComponentType>(
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;
}
Loading
Loading