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

App Bar implementation #11

Merged
merged 18 commits into from
Apr 15, 2021
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
6 changes: 3 additions & 3 deletions client/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ module.exports = {

// Unit tests
{
files: ['./src/**/*.test.ts', './jest/**/*.ts'],
extends: [configs.typescript, configs.dev, configs.tests],
files: ['./src/**/*.test.ts{,x}', './jest/**/*.ts'],
extends: [configs.typescript, configs.react, configs.dev, configs.tests],
},

// E2E tests
Expand All @@ -43,7 +43,7 @@ module.exports = {

// TypeScript and React source code.
{
files: ['./src/**/*.ts', './src/**/*.tsx'],
files: ['./src/**/*.ts{,x}'],
extends: [configs.typescript, configs.react],
},

Expand Down
8 changes: 5 additions & 3 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@
"postinstall": "cd .. && husky install",
"start": "next start -p 8080",
"test": "jest -c jest/test.config.js",
"test:watch": "yarn jest --watch",
"test:update": "yarn jest --updateSnapshot"
"test:watch": "yarn test --watch",
"test:update": "yarn test --updateSnapshot"
},
"dependencies": {
"axios": "^0.21.1",
"clsx": "^1.1.1",
"framer-motion": "^4.1.3",
"next": "10.1.3",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-query": "^3.13.5"
"react-query": "^3.13.5",
"react-use": "^17.2.3"
},
"devDependencies": {
"@babel/core": "^7.13.15",
Expand Down
4 changes: 4 additions & 0 deletions client/public/icons/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions client/public/icons/menu.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions client/public/icons/search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions client/src/components/AppBar/AppBar.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.links {
:not(:first-child) {
@apply ml-napari-sm;
}
}
9 changes: 9 additions & 0 deletions client/src/components/AppBar/AppBar.module.scss.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type Styles = {
links: string;
};

export type ClassNames = keyof Styles;

declare const styles: Styles;

export default styles;
10 changes: 10 additions & 0 deletions client/src/components/AppBar/AppBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { render } from '@testing-library/react';

import { AppBar } from './AppBar';

describe('<AppBar />', () => {
it('should match snapshot', () => {
const component = render(<AppBar />);
expect(component.asFragment()).toMatchSnapshot();
});
});
136 changes: 136 additions & 0 deletions client/src/components/AppBar/AppBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import clsx from 'clsx';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does clsx do? I'm not exactly sure I understand it completely from the package description...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clsx is a smaller alternative to classnames. I think you would understand the functionality of clsx if you read that README instead.

Its purpose is to apply class names to a single string. For example, something like clsx('flex', 'flex-auto', 'justify-center') will output 'flex flex-auto justify-center'. It's mostly useful for conditionally applying styles. For example, if I want to add a visible class if the isVisible state is true:

<div className={clsx('foobar', isVisible && 'visible')} />

For this PR, I found it's useful for Tailwind classes because you can split up extremely long class name strings into multiple lines, which was the main reason why I wanted to move it to SCSS originally.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably a good thing to talk about in the frontend workshop too 😄

import Image from 'next/image';
import { ReactElement, useState } from 'react';

import { MenuDrawer, SearchBar } from '@/components';
import { Link } from '@/components/common';
import { MenuDrawerItem } from '@/components/MenuDrawer/types';

import styles from './AppBar.module.scss';

const IMAGE_SIZE = 16;

const MENU_ITEMS: MenuDrawerItem[] = [
{
title: 'About',
link: '/about',
},
{
title: 'Help',
link: '/help',
},
];

/**
* Header that links back to the home page.
*/
function AppBarHeader() {
return (
<header className="flex">
<h1 className="text-napari-app-bar whitespace-nowrap">
<Link href="/">
napari <strong>hub</strong>
</Link>
</h1>
</header>
);
}

/**
* Link bar for rendering menu links. This only shows up on lg+ screens.
*/
function AppBarLinks() {
return (
<div
className={clsx(
// Hide links on smaller layouts
'hidden lg:flex',

// Margins
'ml-napari-lg',

// Custom link styling
styles.links,
)}
>
{MENU_ITEMS.map((item) => (
<Link key={item.link} href={item.link}>
{item.title}
</Link>
))}
</div>
);
}

/**
* App bar component that renders the home link, search bar, and menu.
*/
export function AppBar(): ReactElement {
const [visible, setVisible] = useState(false);

return (
<>
<MenuDrawer
items={MENU_ITEMS}
onMenuClose={() => setVisible(false)}
visible={visible}
/>

<nav
className={clsx(
// Color and height
'bg-napari-primary h-napari-app-bar',

// Padding
'px-napari-sm md:px-napari-lg 2xl:p-0',

// Grid layout
'grid grid-cols-napari-nav-mobile',
'justify-center items-center',

// Grid gap
'gap-napari-sm md:gap-napari-lg',

// Change to 2 column grid layout when 2xl+ screens
'2xl:grid 2xl:grid-cols-napari-2-col',

// Use 3 column layout when 3xl+ screens
'3xl:grid-cols-napari-3-col',
)}
>
<AppBarHeader />

<div
className={clsx(
// Flex layout
'flex items-center',

// Take 100% of width, but limit to center column width.
'w-full max-w-napari-center-col',

// Align container to the right of the grid cell
'justify-self-end',
)}
>
<SearchBar />
<AppBarLinks />

{/* Menu button */}
<button
// Show menu button on smaller layouts
className="ml-napari-sm flex lg:hidden"
onClick={() => setVisible(true)}
type="button"
>
<Image
src="/icons/menu.svg"
alt="Menu button icon"
width={IMAGE_SIZE}
height={IMAGE_SIZE}
/>
</button>
</div>
</nav>
</>
);
}
136 changes: 136 additions & 0 deletions client/src/components/AppBar/__snapshots__/AppBar.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<AppBar /> should match snapshot 1`] = `
<DocumentFragment>
<div
class="fixed top-0 right-0 flex flex-row items-start z-40 w-9/12 h-screen p-napari-sm bg-black"
data-testid="menu"
style="transform: translateX(100%) translateZ(0);"
>
<ul
class="flex flex-auto flex-col"
>
<li
class="text-white item"
data-testid="drawerItem"
>
<a
href="/about"
>
About
</a>
</li>
<li
class="text-white item"
data-testid="drawerItem"
>
<a
href="/help"
>
Help
</a>
</li>
</ul>
<button
class="flex"
data-testid="drawerClose"
type="button"
>
<div
style="overflow: hidden; box-sizing: border-box; display: inline-block; position: relative; width: 16px; height: 16px;"
>
<noscript />
<img
alt="Menu close button"
decoding="async"
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
style="position: absolute; top: 0px; left: 0px; bottom: 0px; right: 0px; box-sizing: border-box; padding: 0px; margin: auto; display: block; width: 0px; height: 0px; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%;"
/>
</div>
</button>
</div>
<nav
class="bg-napari-primary h-napari-app-bar px-napari-sm md:px-napari-lg 2xl:p-0 grid grid-cols-napari-nav-mobile justify-center items-center gap-napari-sm md:gap-napari-lg 2xl:grid 2xl:grid-cols-napari-2-col 3xl:grid-cols-napari-3-col"
>
<header
class="flex"
>
<h1
class="text-napari-app-bar whitespace-nowrap"
>
<a
href="/"
>
napari
<strong>
hub
</strong>
</a>
</h1>
</header>
<div
class="flex items-center w-full max-w-napari-center-col justify-self-end"
>
<form
class="flex flex-auto items-center border-b-2 border-black"
>
<input
class="flex flex-auto border-none outline-none bg-transparent text-napari-app-bar w-0"
/>
<div
style="overflow: hidden; box-sizing: border-box; display: inline-block; position: relative; width: 14px; height: 14px;"
>
<noscript />
<img
alt="Icon for napari search bar"
decoding="async"
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
style="position: absolute; top: 0px; left: 0px; bottom: 0px; right: 0px; box-sizing: border-box; padding: 0px; margin: auto; display: block; width: 0px; height: 0px; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%;"
/>
</div>
</form>
<div
class="hidden lg:flex ml-napari-lg links"
>
<a
href="/about"
>
About
</a>
<a
href="/help"
>
Help
</a>
</div>
<button
class="ml-napari-sm flex lg:hidden"
type="button"
>
<div
style="display: inline-block; max-width: 100%; overflow: hidden; position: relative; box-sizing: border-box; margin: 0px;"
>
<div
style="box-sizing: border-box; display: block; max-width: 100%;"
>
<img
alt=""
aria-hidden="true"
role="presentation"
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmVyc2lvbj0iMS4xIi8+"
style="max-width: 100%; display: block; margin: 0px; padding: 0px;"
/>
</div>
<noscript />
<img
alt="Menu button icon"
decoding="async"
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
style="position: absolute; top: 0px; left: 0px; bottom: 0px; right: 0px; box-sizing: border-box; padding: 0px; margin: auto; display: block; width: 0px; height: 0px; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%;"
/>
</div>
</button>
</div>
</nav>
</DocumentFragment>
`;
1 change: 1 addition & 0 deletions client/src/components/AppBar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AppBar';
9 changes: 8 additions & 1 deletion client/src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { ReactNode } from 'react';

import { AppBar } from '@/components';

interface Props {
children: ReactNode;
}

export function Layout({ children }: Props) {
return <main>{children}</main>;
return (
<>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this empty tag do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is known as a fragment, and it's used to return multiple elements for a single component.

React has a limitation where you can only return one element from a component. This used to lead developers having to return a lot of wrapper divs:

function SomeComponent() {
  return (
    <div>
      <Foo />
      <Bar />
    </div>
  );
}

Later on, React released Fragments so you could wrap multiple components under a single tag:

import { Fragment } from 'react';

function SomeComponent() {
  return (
    <Fragment>
      <Foo />
      <Bar />
    </Fragment>
  );
}

The shorthand syntax is the empty brackets, and is useful when you don't want to import the entire Fragment component:

function SomeComponent() {
  return (
    <>
      <Foo />
      <Bar />
    </>
  );
}

As a result, this will return the HTML for <Foo /> and <Bar /> without including a wrapper <div />

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, cool! Thanks for explaining 😄

<AppBar />
<main>{children}</main>
</>
);
}
Loading