Skip to content

Commit

Permalink
[redesign] implement new design for tabs (#2592)
Browse files Browse the repository at this point in the history
* implement new design for tabs

* add back `tab` role
  • Loading branch information
thomasheyenbrock committed Aug 9, 2022
1 parent e11ba9a commit 220cf8d
Show file tree
Hide file tree
Showing 17 changed files with 310 additions and 463 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`, `TabAddButton`, `Tabs`, `UnStyledButton` and lots of icon components)
- UI components (`Button`, `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
11 changes: 11 additions & 0 deletions .changeset/spotty-fans-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'graphiql': major
---

BREAKING: Tabs are now always enabled. The `tabs` prop has therefore been replaced with a prop `onTabChange`. If you used the `tabs` prop before to pass this function you can change your implementation like so:
```diff
<GraphiQL
- tabs={{ onTabChange: (tabState) => {/* do something */} }}
+ onTabChange={(tabState) => {/* do something */}}
/>
```
2 changes: 2 additions & 0 deletions packages/graphiql-react/src/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import _MagnifyingGlassIcon from './magnifying-glass.svg';
import _MergeIcon from './merge.svg';
import _PenIcon from './pen.svg';
import _PlayIcon from './play.svg';
import _PlusIcon from './plus.svg';
import _PrettifyIcon from './prettify.svg';
import _ReloadIcon from './reload.svg';
import _RootTypeIcon from './root-type.svg';
Expand Down Expand Up @@ -68,6 +69,7 @@ export const MagnifyingGlassIcon = generateIcon(
export const MergeIcon = generateIcon(_MergeIcon, 'merge icon');
export const PenIcon = generateIcon(_PenIcon, 'pen icon');
export const PlayIcon = generateIcon(_PlayIcon, 'play icon');
export const PlusIcon = generateIcon(_PlusIcon, 'plus icon');
export const PrettifyIcon = generateIcon(_PrettifyIcon, 'prettify icon');
export const ReloadIcon = generateIcon(_ReloadIcon, 'reload icon');
export const RootTypeIcon = generateIcon(_RootTypeIcon, 'root type icon');
Expand Down
3 changes: 3 additions & 0 deletions packages/graphiql-react/src/icons/plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/graphiql-react/src/style/root.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
/* Layout */
--sidebar-width: 44px;
--toolbar-width: 40px;
--session-header-height: 51px;
}

.graphiql-container,
Expand Down
45 changes: 45 additions & 0 deletions packages/graphiql-react/src/ui/tabs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.graphiql-tabs {
display: flex;
overflow-y: auto;
padding: var(--px-12);

& > :not(:first-child) {
margin-left: var(--px-12);
}
}

.graphiql-tab {
align-items: stretch;
border-radius: var(--border-radius-8);
color: var(--color-neutral-60);
display: flex;

& > button.graphiql-tab-close {
visibility: hidden;
}
&.graphiql-tab-active > button.graphiql-tab-close,
&:hover > button.graphiql-tab-close,
&:focus-within > button.graphiql-tab-close {
visibility: unset;
}

&.graphiql-tab-active {
background-color: var(--color-neutral-15);
color: var(--color-neutral-100);
}
}

button.graphiql-tab-button {
padding: var(--px-4) 0 var(--px-4) var(--px-8);
}

button.graphiql-tab-close {
align-items: center;
display: flex;
padding: var(--px-4) var(--px-8);

& > svg {
height: var(--px-8);
width: var(--px-8);
}
}
100 changes: 51 additions & 49 deletions packages/graphiql-react/src/ui/tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,70 @@
function TabCloseButton(props: { onClick: () => void }) {
return (
<div
role="button"
aria-pressed={false}
className="close"
aria-label="Close Tab"
title="Close Tab"
onClick={ev => {
ev.stopPropagation();
props.onClick();
}}
/>
);
}
import { CloseIcon } from '../icons';
import { compose } from '../utility/compose';
import { UnStyledButton } from './button';

import './tabs.css';

type TabProps = {
isActive: boolean;
title: string;
isCloseable: boolean;
onSelect: () => void;
onClose: () => void;
tabProps?: React.ButtonHTMLAttributes<{}>;
isActive?: boolean;
};

/**
* Generic tab component that implements wai-aria tab spec
*/
export function Tab(props: TabProps): React.ReactElement {
export function Tab({
isActive,
...props
}: TabProps & JSX.IntrinsicElements['div']) {
return (
<button
{...props.tabProps}
<div
{...props}
role="tab"
aria-selected={isActive}
className={compose(
'graphiql-tab',
isActive ? 'graphiql-tab-active' : '',
props.className,
)}
>
{props.children}
</div>
);
}

function TabButton(props: JSX.IntrinsicElements['button']) {
return (
<UnStyledButton
{...props}
type="button"
aria-selected={props.isActive}
title={props.title}
className={`tab${props.isActive ? ' active' : ''}`}
onClick={props.onSelect}
className={compose('graphiql-tab-button', props.className)}
>
{props.title}
{props.isCloseable ? (
<TabCloseButton onClick={() => props.onClose()} />
) : null}
</button>
{props.children}
</UnStyledButton>
);
}

export function TabAddButton(props: { onClick: () => void }) {
Tab.Button = TabButton;

function TabClose(props: JSX.IntrinsicElements['button']) {
return (
<button onClick={props.onClick} className="tab-add" title="Create new tab">
<span>+</span>
</button>
<UnStyledButton
aria-label="Close Tab"
title="Close Tab"
{...props}
type="button"
className={compose('graphiql-tab-close', props.className)}
>
<CloseIcon />
</UnStyledButton>
);
}

type TabsProps = {
children: Array<React.ReactNode>;
tabsProps?: React.HTMLAttributes<{}>;
};
/**
* Generic tablist component that implements wai-aria tab spec
*/
export function Tabs(props: TabsProps) {
Tab.Close = TabClose;

export function Tabs(props: JSX.IntrinsicElements['div']) {
return (
<div role="tablist" className="tabs" {...props.tabsProps}>
<div
{...props}
role="tablist"
className={compose('graphiql-tabs', props.className)}
>
{props.children}
</div>
);
Expand Down
9 changes: 9 additions & 0 deletions packages/graphiql-react/src/utility/compose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function compose(...classes: (string | null | undefined)[]) {
let result = '';
for (const c of classes) {
if (c) {
result += (result ? ' ' : '') + c;
}
}
return result;
}
2 changes: 1 addition & 1 deletion packages/graphiql/__mocks__/@graphiql/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export {
onHasCompletion,
PenIcon,
PlayIcon,
PlusIcon,
PrettifyIcon,
ReloadIcon,
RootTypeIcon,
Expand All @@ -70,7 +71,6 @@ export {
StorageContext,
StorageContextProvider,
Tab,
TabAddButton,
Tabs,
ToolbarButton,
TypeDocumentation,
Expand Down
3 changes: 1 addition & 2 deletions packages/graphiql/cypress/integration/init.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ describe('GraphiQL On Initialization', () => {
const containers = [
'#graphiql',
'.graphiql-container',
'.topBarWrap',
'.editorWrap',
'.graphiql-sessions',
'.graphiql-editors',
'.graphiql-response',
'.graphiql-editor-tool',
Expand Down
27 changes: 14 additions & 13 deletions packages/graphiql/cypress/integration/tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
describe('Tabs', () => {
it('Should store editor contents when switching between tabs', () => {
cy.visit('/?query=');
cy.get('#session-tab-0').should('have.text', '<untitled>');

// Assert that no tab visible when there's only one session
cy.get('#graphiql-session-tab-0').should('not.exist');

// Enter a query without operation name
cy.get('.graphiql-query-editor textarea')
.type('{id', { force: true })
.wait(500);
cy.get('#session-tab-0').should('have.text', '<untitled>');

// Run the query
cy.clickExecuteQuery().wait(500);

// Open a new tab
cy.get('.tab-add').click();
cy.get('.graphiql-tab-add').click();

// Enter a query
cy.get('.graphiql-query-editor textarea')
.type('query Foo {image', { force: true })
.wait(500);
cy.get('#session-tab-1').should('have.text', 'Foo');
cy.get('#graphiql-session-tab-1').should('have.text', 'Foo');

// Enter variables
cy.get('.graphiql-editor-tool textarea')
Expand All @@ -36,11 +37,11 @@ describe('Tabs', () => {
cy.clickExecuteQuery().wait(500);

// Switch back to the first tab
cy.get('#session-tab-0').click();
cy.get('#graphiql-session-tab-0').click();

// Assert tab titles
cy.get('#session-tab-0').should('have.text', '<untitled>');
cy.get('#session-tab-1').should('have.text', 'Foo');
cy.get('#graphiql-session-tab-0').should('have.text', '<untitled>');
cy.get('#graphiql-session-tab-1').should('have.text', 'Foo');

// Assert editor values
cy.assertHasValues({
Expand All @@ -51,11 +52,11 @@ describe('Tabs', () => {
});

// Switch back to the second tab
cy.get('#session-tab-1').click();
cy.get('#graphiql-session-tab-1').click();

// Assert tab titles
cy.get('#session-tab-0').should('have.text', '<untitled>');
cy.get('#session-tab-1').should('have.text', 'Foo');
cy.get('#graphiql-session-tab-0').should('have.text', '<untitled>');
cy.get('#graphiql-session-tab-1').should('have.text', 'Foo');

// Assert editor values
cy.assertHasValues({
Expand All @@ -66,10 +67,10 @@ describe('Tabs', () => {
});

// Close tab
cy.get('#session-tab-1 .close').click();
cy.get('#graphiql-session-tab-1 + .graphiql-tab-close').click();

// Assert tab titles
cy.get('#session-tab-0').should('have.text', '<untitled>');
// Assert that no tab visible when there's only one session
cy.get('#graphiql-session-tab-0').should('not.exist');

// Assert editor values
cy.assertHasValues({
Expand Down
4 changes: 1 addition & 3 deletions packages/graphiql/resources/renderExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,7 @@ ReactDOM.render(
headerEditorEnabled: true,
shouldPersistHeaders: true,
inputValueDeprecation: GraphQLVersion.includes('15.5') ? undefined : true,
tabs: {
onTabChange: onTabChange,
},
onTabChange,
}),
document.getElementById('graphiql'),
);
3 changes: 0 additions & 3 deletions packages/graphiql/src/cdn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import '@graphiql/react/font/fira-code.css';
import '@graphiql/react/dist/style.css';
import './style.css';

// Legacy styles
import './css/app.css';

import { GraphiQL } from './components/GraphiQL';
// add the static function here for CDN only. otherwise, doing this in the component could
// add unwanted dependencies to the bundle.
Expand Down
Loading

0 comments on commit 220cf8d

Please sign in to comment.