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

Conversation

codemonkey800
Copy link
Collaborator

Description

This PR implements the App Bar for the napari hub. The styling is very similar to napari/napari.github.io#122, using the same 2-column and 3-column layout for styling the app bar.

Changes

  • Minor fixes: ESlint, package.json scripts, and making the viewport responsive
  • Added new packages:
  • Make page scroll by default
    • The page scroll bar takes about 20px of space.
    • This will cause a visible layout shift when switching between pages with scrollable and non-scrollable content, so I added a scroll bar by default
  • Added components with unit and snapshot tests:
    • Link
    • SearchBar
    • Overlay
    • MenuDrawer
    • AppBar

Demos

Slide Out Menu

menu-drawer.mp4

Responsive App Bar

app-bar-responsive.mp4

Demo Code

./src/pages/index.tsx
/* eslint-disable no-param-reassign */

import clsx from 'clsx';
import { useState } from 'react';

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

const COLORS = ['red', 'blue', 'green', 'purple', 'cyan'];

function shuffle(nums: string[]) {
  const result = [...nums];
  let index = nums.length - 1;

  while (index > 0) {
    const randIndex = Math.floor(Math.random() * index);
    const tmp = result[randIndex];
    result[randIndex] = result[index];
    result[index] = tmp;
    index -= 1;
  }

  return result;
}

interface BarsProps {
  className?: string;
}

function Bars({ className }: BarsProps) {
  const [colors] = useState(shuffle(COLORS));

  return (
    <div className={clsx(styles.box, className)}>
      {colors.map((color) => (
        <h1
          key={color}
          data-napari-test="homeText"
          style={{
            background: color,
          }}
        >
          Hello, World!
        </h1>
      ))}
    </div>
  );
}

export default function Home() {
  return (
    <div className={styles.container}>
      <Bars className={styles.firstColumn} />
      <Bars />
      <Bars className={styles.thirdColumn} />
    </div>
  );
}
./src/pages/about.tsx
export default function About() {
  return (
    <h1
      style={{
        width: '100vw',
        height: 'calc(100vh - 75px)',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
      }}
    >
      About Page!
    </h1>
  );
}
./src/pages/index.module.scss
.container {
  @apply justify-center items-center;

  // Grid gap
  @apply gap-napari-sm md:gap-napari-lg;

  // Change to 2 column grid layout when (screen >= 2xl)
  @apply 2xl:grid 2xl:grid-cols-napari-2-col;

  // Use 3 column layout when (screen >= 3xl)
  @apply 3xl:grid-cols-napari-3-col;
}

.box {
  @apply flex flex-auto flex-col;

  h1 {
    @apply flex flex-auto items-center justify-center;
    height: 300px;
  }
}

.firstColumn {
  @apply hidden;
  @apply 2xl:flex;
}

.thirdColumn {
  @apply hidden;
  @apply 3xl:flex;
}

@codemonkey800
Copy link
Collaborator Author

lolol I don't know how, but this PR jumped from #3 to #11 somehow 😆

@liu-ziyang
Copy link
Contributor

lolol I don't know how, but this PR jumped from #3 to #11 somehow 😆

github tracks issues with prs together, I think the recently opened issues claimed those numbers

Copy link
Member

@kne42 kne42 left a comment

Choose a reason for hiding this comment

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

LGTM from my very limited understanding of React 😅 Just a few small questions/comments again :)

@@ -0,0 +1 @@
export * from './Link';
Copy link
Member

Choose a reason for hiding this comment

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

I feel like making a whole "subpackage" (idk what you call it in js) for such a small component seems overkill. Maybe this belongs in MenuDrawer, or, if it's more generic, in some utility package.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's a module. Modules can be directories if it has an index.ts, or it can be its own file. We'll go over it our TypeScript workshop 😄


I don't think there's necessarily a limit to how large a component needs to be. As an example, here's probably the smallest component I could find within CZI: https://git.io/JOGXP

The only difference is that we have an index.ts exporting the component and a unit test 😆 The reason I don't define components in the index.ts file is because:

  1. I like thinking of the index.ts file as responsible for exporting exports from submodules.
  2. Having a named file like Link.tsx is easier to work with than index.ts. One example I can think of is imagine having multiple components open in your editor, and they're all named index.ts. Your editor would likely add the directory as part of the filename to discern the two. For example, this is what it looks like in VSCode:
    image
  3. Probably a bunch more reasons

The test might be a bit overkill, but all it really does is check for children and makes sure the snapshot is the same, so we're not doing a lot there.


You do raise a good point about having a utility package. I went ahead and created the components/common directory for shared components. So far I've moved the Overlay and Link components there lol

height: {
'napari-app-bar': '75px',
},

gridTemplateColumns: {
'napari-nav-mobile': 'min-content 1fr',
Copy link
Member

Choose a reason for hiding this comment

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

what's this 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 creates a 2 column grid where the first column is only as wide as the content, and the second column fills up the rest of the space.

Interestingly the result would have been the same if we used min-content or max-content because we're purposely not wrapping the header.


Fractional units are interesting, but they basically specify what fraction of the remaining space should a grid column take. For example:

  • If the grid is one column with 1fr, then the column will take 100% of the space
  • If the grid is two columns with 1fr 1fr, then both columns will take 50% of the space
  • If the grid is two columns with 1fr 2fr, then the first column will take 33% of the space and the second column will take 67% of the space

It's mostly a convenience over having to manually calculate the percentages for grid columns. I included some references for more info below:

References:

Copy link
Member

@kne42 kne42 Apr 14, 2021

Choose a reason for hiding this comment

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

oh, so if I understand correctly, this is similar to flex weight then?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If you're referring to flex-grow, then yes it is 😸

Copy link
Contributor

@justinelarsen justinelarsen left a comment

Choose a reason for hiding this comment

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

Looks great overall! I just added a minor comment.

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 😄

<button data-testid="drawerClose" onClick={onMenuClose} type="button">
<Image
src="/icons/close.svg"
alt="Icon for napari search bar"
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be something like "Menu close button" instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds good, perhaps I was being too technical since it's not actually the button element, but it makes more sense to add it 😆

@codemonkey800
Copy link
Collaborator Author

@justinelarsen @kne42 I made a change to the PR that moves almost all of the Tailwind utilities into the component class name. I did a comparison on the the CSS output, and doing it this way generates a smaller CSS file than having everything as its own class in a CSS module. I went ahead and removed most of the CSS module files, but left a couple since there's no equivalent in Tailwind.

I think going forward, we should try to use Tailwind class names as much as possible, and reserve custom CSS for things Tailwind can't do. For example, using the :not(:last-child) selector.

@justinelarsen
Copy link
Contributor

justinelarsen commented Apr 14, 2021

@justinelarsen @kne42 I made a change to the PR that moves almost all of the Tailwind utilities into the component class name. I did a comparison on the the CSS output, and doing it this way generates a smaller CSS file than having everything as its own class in a CSS module. I went ahead and removed most of the CSS module files, but left a couple since there's no equivalent in Tailwind.

Fine with me if it makes the files smaller. Would love to hear what @kne42 thinks about it.

@codemonkey800
Copy link
Collaborator Author

Another big reason is that the @apply approach is technically not using Tailwind properly, but I didn't realize that until now. When using the @apply directive, it's' actually just copying the CSS to the class, so it's no different from writing the CSS by hand. When using Tailwind in the class name, we're actually reusing the styles

@@ -1,3 +1,4 @@
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 😄

@codemonkey800 codemonkey800 merged commit b4c6b97 into chanzuckerberg:main Apr 15, 2021
@codemonkey800 codemonkey800 deleted the jeremy/header branch April 15, 2021 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants