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

Allow GraphiQL apps control over closing tabs #3563

Merged
merged 11 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/empty-lobsters-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphiql': minor
---

Add new prop `confirmCloseTab` to allow control of closing tabs
24 changes: 24 additions & 0 deletions packages/graphiql/cypress/e2e/tabs.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,28 @@ describe('Tabs', () => {
response: { data: { id: 'abc123' } },
});
});

describe('confirmCloseTab()', () => {
it('should keep tab when `Cancel` was clicked', () => {
cy.on('window:confirm', () => false);
cy.visit('/?confirmCloseTab=true');

cy.get('.graphiql-tab-add').click();

cy.get('.graphiql-tab-button + .graphiql-tab-close').eq(1).click();

cy.get('.graphiql-tab-button').should('have.length', 2);
});

it('should close tab when `OK` was clicked', () => {
cy.on('window:confirm', () => true);
cy.visit('/?confirmCloseTab=true');

cy.get('.graphiql-tab-add').click();

cy.get('.graphiql-tab-button + .graphiql-tab-close').eq(1).click();

cy.get('.graphiql-tab-button').should('have.length', 0);
});
});
});
21 changes: 11 additions & 10 deletions packages/graphiql/resources/renderExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,14 @@
*
* It is used by:
* - the netlify demo
* - end to end tests
* - end-to-end tests
* - webpack dev server
*/

// Parse the search string to get url parameters.
const parameters = {};
for (const entry of window.location.search.slice(1).split('&')) {
const eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] = decodeURIComponent(
entry.slice(eq + 1),
);
}
}
const parameters = Object.fromEntries(
new URLSearchParams(location.search).entries(),
);

// When the query and variables string is edited, update the URL bar so
// that it can be easily shared.
Expand All @@ -48,6 +42,11 @@ function onTabChange(tabsState) {
updateURL();
}

function confirmCloseTab(index) {
// eslint-disable-next-line no-alert
return confirm(`Are you sure you want to close tab with index ${index}?`);
}

function updateURL() {
const newSearch = Object.entries(parameters)
.filter(([_key, value]) => value)
Expand Down Expand Up @@ -101,6 +100,8 @@ root.render(
isHeadersEditorEnabled: true,
shouldPersistHeaders: true,
inputValueDeprecation: GraphQLVersion.includes('15.5') ? undefined : true,
confirmCloseTab:
parameters.confirmCloseTab === 'true' ? confirmCloseTab : undefined,
onTabChange,
forcedTheme: parameters.forcedTheme,
}),
Expand Down
86 changes: 64 additions & 22 deletions packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@
*
* @see https://github.com/graphql/graphiql#usage
*/

export function GraphiQL({
dangerouslyAssumeSchemaIsValid,
confirmCloseTab,
defaultQuery,
defaultTabs,
externalFragments,
Expand Down Expand Up @@ -168,6 +168,7 @@
variables={variables}
>
<GraphiQLInterface
confirmCloseTab={confirmCloseTab}
showPersistHeadersSettings={shouldPersistHeaders !== false}
disableTabs={props.disableTabs ?? false}
forcedTheme={props.forcedTheme}
Expand Down Expand Up @@ -220,19 +221,29 @@
showPersistHeadersSettings?: boolean;
disableTabs?: boolean;
/**
* forcedTheme allows enforcement of a specific theme for GraphiQL.
* `forcedTheme` allows enforcement of a specific theme for GraphiQL.
* This is useful when you want to make sure that GraphiQL is always
* rendered with a specific theme
* rendered with a specific theme.
*/
forcedTheme?: (typeof THEMES)[number];
/**
* Additional class names which will be appended to the container element.
*/
className?: string;
/**
* When the user clicks a close tab button, this function is invoked with
* the index of the tab that is about to be closed. It can return a promise
* that should resolve to `true` (meaning the tab may be closed) or `false`
* (meaning the tab may not be closed).
* @param index The index of the tab that should be closed.
*/
confirmCloseTab?(index: number): Promise<boolean> | boolean;
};

const THEMES = ['light', 'dark', 'system'] as const;

const TAB_CLASS_PREFIX = 'graphiql-session-tab-';

export function GraphiQLInterface(props: GraphiQLInterfaceProps) {
const isHeadersEditorEnabled = props.isHeadersEditorEnabled ?? true;
const editorContext = useEditorContext({ nonNull: true });
Expand Down Expand Up @@ -408,9 +419,9 @@
);

const handlePluginClick: MouseEventHandler<HTMLButtonElement> = useCallback(
e => {
event => {
const context = pluginContext!;
const pluginIndex = Number(e.currentTarget.dataset.index!);
const pluginIndex = Number(event.currentTarget.dataset.index!);
const plugin = context.plugins.find((_, index) => pluginIndex === index)!;
const isVisible = plugin === context.visiblePlugin;
if (isVisible) {
Expand Down Expand Up @@ -470,6 +481,48 @@
);

const className = props.className ? ` ${props.className}` : '';
const confirmClose = props.confirmCloseTab;

const handleTabClose: MouseEventHandler<HTMLButtonElement> = useCallback(
async event => {
const tabButton = event.currentTarget
.previousSibling as HTMLButtonElement;
const index = Number(tabButton.id.replace(TAB_CLASS_PREFIX, ''));

/** TODO:
* Move everything after into `editorContext.closeTab` once zustand will be used instead of
* React context, since now we can't use execution context inside editor context, since editor
* context is used in execution context.
*/
const shouldCloseTab = confirmClose ? await confirmClose(index) : true;

if (!shouldCloseTab) {
return;

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L500 was not covered by tests
}

if (editorContext.activeTabIndex === index) {
executionContext.stop();

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L504 was not covered by tests
}
editorContext.closeTab(index);
},
[confirmClose, editorContext, executionContext],
);

const handleTabClick: MouseEventHandler<HTMLButtonElement> = useCallback(
event => {
const index = Number(

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

View check run for this annotation

Codecov / codecov/patch

packages/graphiql/src/components/GraphiQL.tsx#L512-L513

Added lines #L512 - L513 were not covered by tests
event.currentTarget.id.replace(TAB_CLASS_PREFIX, ''),
);
/** TODO:
* Move everything after into `editorContext.changeTab` once zustand will be used instead of
* React context, since now we can't use execution context inside editor context, since editor
* context is used in execution context.
*/
executionContext.stop();
editorContext.changeTab(index);

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

View check run for this annotation

Codecov / codecov/patch

packages/graphiql/src/components/GraphiQL.tsx#L521-L522

Added lines #L521 - L522 were not covered by tests
},
[editorContext, executionContext],
);

return (
<Tooltip.Provider>
Expand All @@ -482,7 +535,6 @@
{pluginContext?.plugins.map((plugin, index) => {
const isVisible = plugin === pluginContext.visiblePlugin;
const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}`;
const Icon = plugin.icon;
return (
<Tooltip key={plugin.title} label={label}>
<UnStyledButton
Expand All @@ -492,7 +544,7 @@
data-index={index}
aria-label={label}
>
<Icon aria-hidden="true" />
<plugin.icon aria-hidden="true" />
</UnStyledButton>
</Tooltip>
);
Expand Down Expand Up @@ -571,22 +623,12 @@
>
<Tab.Button
aria-controls="graphiql-session"
id={`graphiql-session-tab-${index}`}
onClick={() => {
executionContext.stop();
editorContext.changeTab(index);
}}
id={`${TAB_CLASS_PREFIX}${index}`}
onClick={handleTabClick}
>
{tab.title}
</Tab.Button>
<Tab.Close
onClick={() => {
if (editorContext.activeTabIndex === index) {
executionContext.stop();
}
editorContext.closeTab(index);
}}
/>
<Tab.Close onClick={handleTabClose} />
</Tab>
))}
{addTab}
Expand All @@ -601,9 +643,9 @@
</div>
<div
role="tabpanel"
id="graphiql-session"
id="graphiql-session" // used by aria-controls="graphiql-session"
className="graphiql-session"
aria-labelledby={`graphiql-session-tab-${editorContext.activeTabIndex}`}
aria-labelledby={`${TAB_CLASS_PREFIX}${editorContext.activeTabIndex}`}
>
<div ref={editorResize.firstRef}>
<div
Expand Down
1 change: 1 addition & 0 deletions resources/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ wgutils
wincent
wonka
yoshiakis
zustand
zdravo
Здорово
أهلاً
Expand Down