Skip to content

Commit

Permalink
[redesign] add settings dialog (#2595)
Browse files Browse the repository at this point in the history
* add `Dialog` component to `@graphiql/react`

* add a `clear` method to the `Storage` type

* add success and failure states to `Button` component

* add settings dialog

* make sure to show dialog above editor scrollbars
  • Loading branch information
thomasheyenbrock committed Aug 24, 2022
1 parent 7f7b916 commit 9cae979
Show file tree
Hide file tree
Showing 14 changed files with 339 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .changeset/five-pillows-fail.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
---

Add new components:
- UI components (`Button`, `Dropdown`, `Spinner`, `Tab`, `Tabs`, `UnStyledButton` and lots of icon components)
- UI components (`Button`, `Dialog`, `Dropdown`, `Spinner`, `Tab`, `Tabs`, `UnStyledButton` and lots of icon components)
- Editor components (`QueryEditor`, `VariableEditor`, `HeaderEditor` and `ResponseEditor`)
- Toolbar components (`ExecuteButton` and `ToolbarButton`)
- Docs components (`Argument`, `DefaultValue`, `DeprecationReason`, `Directive`, `DocExplorer`, `ExplorerSection`, `FieldDocumentation`, `FieldLink`, `SchemaDocumentation`, `Search`, `TypeDocumentation` and `TypeLink`)
Expand Down
5 changes: 5 additions & 0 deletions .changeset/tender-impalas-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphiql/toolkit': minor
---

Add a `clear` method to `Storage` classes
2 changes: 2 additions & 0 deletions packages/graphiql-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"dependencies": {
"@graphiql/toolkit": "^1.0.0-next.1",
"@reach/combobox": "^0.17.0",
"@reach/dialog": "^0.17.0",
"@reach/visually-hidden": "^0.17.0",
"codemirror": "^5.65.3",
"codemirror-graphql": "^2.0.0-next.1",
"copy-to-clipboard": "^3.2.0",
Expand Down
7 changes: 6 additions & 1 deletion packages/graphiql-react/src/style/root.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.graphiql-container,
.CodeMirror-info,
.CodeMirror-lint-tooltip {
.CodeMirror-lint-tooltip,
reach-portal {
/* Colors */
--color-pink: #d60690;
--color-purple: #6e6acf;
Expand All @@ -23,6 +24,8 @@
--color-orche-background: rgba(211, 127, 0, 0.07);
--color-orche-background-dark: rgba(211, 127, 0, 0.12);
--color-red-background: rgba(248, 91, 48, 0.12);
--color-green-background: rgba(43, 171, 124, 0.12);
--color-neutral-background: rgba(59, 75, 104, 0.1);

/* Font */
--font-family: 'Roboto', sans-serif;
Expand All @@ -46,6 +49,7 @@
--px-12: 12px;
--px-16: 16px;
--px-20: 20px;
--px-24: 24px;

/* Border radius */
--border-radius-2: 2px;
Expand All @@ -67,6 +71,7 @@
.graphiql-container,
.CodeMirror-info,
.CodeMirror-lint-tooltip,
reach-portal,
.graphiql-container:is(button) {
color: var(--color-neutral-100);
font-family: var(--font-family);
Expand Down
7 changes: 7 additions & 0 deletions packages/graphiql-react/src/ui/button.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,11 @@ button.graphiql-button {
&:focus {
outline: var(--color-neutral-15) auto 1px;
}

&.graphiql-button-success {
background-color: var(--color-green-background);
}
&.graphiql-button-error {
background-color: var(--color-red-background);
}
}
18 changes: 15 additions & 3 deletions packages/graphiql-react/src/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { compose } from '../utility/compose';

import './button.css';

export function UnStyledButton(props: JSX.IntrinsicElements['button']) {
return (
<button
{...props}
className={`graphiql-un-styled ${props.className || ''}`.trim()}
className={compose('graphiql-un-styled', props.className)}
/>
);
}

export function Button(props: JSX.IntrinsicElements['button']) {
type ButtonProps = { state?: 'success' | 'error' };

export function Button(props: ButtonProps & JSX.IntrinsicElements['button']) {
return (
<button
{...props}
className={`graphiql-button ${props.className || ''}`.trim()}
className={compose(
'graphiql-button',
props.state === 'success'
? 'graphiql-button-success'
: props.state === 'error'
? 'graphiql-button-error'
: '',
props.className,
)}
/>
);
}
34 changes: 34 additions & 0 deletions packages/graphiql-react/src/ui/dialog.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@import url('@reach/dialog/styles.css');

[data-reach-dialog-overlay] {
align-items: center;
background-color: var(--color-neutral-background);
display: flex;
justify-content: center;
/**
* CodeMirror has a `z-index` set for the container of the scrollbar of the
* editor, so we have to add one here to make sure that the dialog is shown
* above the editor scrollbar (if they are visible).
*/
z-index: 10;
}

[data-reach-dialog-content] {
background-color: var(--color-neutral-0);
border-radius: var(--border-radius-12);
box-shadow: var(--box-shadow);
margin: 0;
max-height: 80vh;
max-width: 80vw;
overflow: auto;
padding: 0;
width: unset;
}

.graphiql-dialog-close > svg {
color: var(--color-neutral-60);
display: block;
height: var(--px-12);
padding: var(--px-12);
width: var(--px-12);
}
27 changes: 27 additions & 0 deletions packages/graphiql-react/src/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Dialog as ReachDialog } from '@reach/dialog';
import { VisuallyHidden } from '@reach/visually-hidden';
import { ComponentProps } from 'react';
import { CloseIcon } from '../icons';
import { compose } from '../utility/compose';
import { UnStyledButton } from './button';

import './dialog.css';

export function Dialog(props: ComponentProps<typeof ReachDialog>) {
return <ReachDialog {...props} />;
}

function DialogClose(props: JSX.IntrinsicElements['button']) {
return (
<UnStyledButton
{...props}
type="button"
className={compose('graphiql-dialog-close', props.className)}
>
<VisuallyHidden>Close dialog</VisuallyHidden>
<CloseIcon />
</UnStyledButton>
);
}

Dialog.Close = DialogClose;
1 change: 1 addition & 0 deletions packages/graphiql-react/src/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './button';
export * from './dialog';
export * from './dropdown';
export * from './markdown';
export * from './spinner';
Expand Down
7 changes: 7 additions & 0 deletions packages/graphiql-toolkit/src/storage/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type Storage = {
getItem(key: string): string | null;
removeItem(key: string): void;
setItem(key: string, value: string): void;
clear(): void;
length: number;
};

Expand Down Expand Up @@ -77,6 +78,12 @@ export class StorageAPI {

return { isQuotaError: quotaError, error };
}

clear() {
if (this.storage) {
this.storage.clear();
}
}
}

const STORAGE_NAMESPACE = 'graphiql';
1 change: 1 addition & 0 deletions packages/graphiql/__mocks__/@graphiql/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export {
DeprecatedEnumValueIcon,
DeprecatedFieldIcon,
DeprecationReason,
Dialog,
Directive,
DirectiveIcon,
DocExplorer,
Expand Down
89 changes: 77 additions & 12 deletions packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,43 @@ import {
} from 'graphql';

import {
Button,
ChevronDownIcon,
ChevronUpIcon,
CopyIcon,
Dialog,
DocExplorer,
DocsIcon,
EditorContextProvider,
EditorContextType,
ExecuteButton,
ExecutionContextProvider,
ExecutionContextType,
ExplorerContextProvider,
ExplorerContextType,
HeaderEditor,
History,
HistoryContextProvider,
HistoryContextType,
HistoryIcon,
KeyboardShortcutIcon,
KeyMap,
MergeIcon,
PlusIcon,
PrettifyIcon,
QueryEditor,
ReloadIcon,
ResponseEditor,
ResponseTooltipType,
SchemaContextProvider,
SchemaContextType,
SettingsIcon,
Spinner,
StorageContextProvider,
StorageContextType,
Tab,
Tabs,
TabsState,
ToolbarButton,
UnStyledButton,
useAutoCompleteLeafs,
Expand All @@ -64,16 +74,6 @@ import {
useStorageContext,
VariableEditor,
} from '@graphiql/react';
import type {
EditorContextType,
ExplorerContextType,
HistoryContextType,
ResponseTooltipType,
SchemaContextType,
StorageContextType,
TabsState,
KeyMap,
} from '@graphiql/react';

import { ToolbarMenu, ToolbarMenuItem } from './ToolbarMenu';
import find from '../utility/find';
Expand Down Expand Up @@ -635,6 +635,8 @@ type GraphiQLWithContextConsumerProps = Omit<

export type GraphiQLState = {
activeSecondaryEditor: 'variable' | 'header';
showSettings: boolean;
clearStorageStatus: 'success' | 'error' | null;
};

class GraphiQLWithContext extends React.Component<
Expand All @@ -645,7 +647,11 @@ class GraphiQLWithContext extends React.Component<
super(props);

// Initialize state
this.state = { activeSecondaryEditor: 'variable' };
this.state = {
activeSecondaryEditor: 'variable',
showSettings: false,
clearStorageStatus: null,
};
}

render() {
Expand Down Expand Up @@ -776,7 +782,12 @@ class GraphiQLWithContext extends React.Component<
<UnStyledButton type="button">
<KeyboardShortcutIcon />
</UnStyledButton>
<UnStyledButton type="button">
<UnStyledButton
type="button"
onClick={() => {
this.setState({ showSettings: true });
}}
>
<SettingsIcon />
</UnStyledButton>
</div>
Expand Down Expand Up @@ -1030,6 +1041,60 @@ class GraphiQLWithContext extends React.Component<
</div>
</div>
</div>
<Dialog
isOpen={this.state.showSettings}
onDismiss={() => {
this.setState({
showSettings: false,
clearStorageStatus: null,
});
}}
>
<div className="graphiql-dialog-header">
<div className="graphiql-dialog-title">Settings</div>
<Dialog.Close
onClick={() => {
this.setState({
showSettings: false,
clearStorageStatus: null,
});
}}
/>
</div>
{this.props.storageContext ? (
<div className="graphiql-dialog-section">
<div>
<div className="graphiql-dialog-section-title">
Clear storage
</div>
<div className="graphiql-dialog-section-caption">
Remove all locally stored data and start fresh.
</div>
</div>
<div>
<Button
type="button"
state={this.state.clearStorageStatus || undefined}
disabled={this.state.clearStorageStatus === 'success'}
onClick={() => {
try {
this.props.storageContext?.clear();
this.setState({ clearStorageStatus: 'success' });
} catch {
this.setState({ clearStorageStatus: 'error' });
}
}}
>
{this.state.clearStorageStatus === 'success'
? 'Cleared data'
: this.state.clearStorageStatus === 'error'
? 'Failed'
: 'Clear data'}
</Button>
</div>
</div>
) : null}
</Dialog>
</div>
);
}
Expand Down
Loading

0 comments on commit 9cae979

Please sign in to comment.