-
Notifications
You must be signed in to change notification settings - Fork 18
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
Changes from all commits
fb29caa
9c5f629
b130708
a7a252b
25866cf
0cfed06
577861d
2a5a9c0
3b62d76
ac2ae13
89a9bd6
f0919c4
9494c1d
947c9d6
481ea38
92d54a5
46909e4
86b8470
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
.links { | ||
:not(:first-child) { | ||
@apply ml-napari-sm; | ||
} | ||
} |
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; |
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(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import clsx from 'clsx'; | ||
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> | ||
</> | ||
); | ||
} |
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> | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './AppBar'; |
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 ( | ||
<> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this empty tag do? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, cool! Thanks for explaining 😄 |
||
<AppBar /> | ||
<main>{children}</main> | ||
</> | ||
); | ||
} |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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 ofclsx
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 avisible
class if theisVisible
state istrue
: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.
There was a problem hiding this comment.
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 😄