Skip to content

Commit

Permalink
Implement navbar (#34)
Browse files Browse the repository at this point in the history
* navbar

* test navbar

* test navbar

* story for disabled, change flow type name, style for disabled item

* add test for click event
  • Loading branch information
moonlight8978 authored Jan 3, 2020
1 parent 0a9b0ec commit 986ac05
Show file tree
Hide file tree
Showing 14 changed files with 441 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ rules:
- allow:
- __REDUX_DEVTOOLS_EXTENSION__
- __REDUX_DEVTOOLS_EXTENSION_COMPOSE__
no-unused-expressions:
- error
- allowShortCircuit: true
no-unused-vars:
- error
- args: none
Expand Down
4 changes: 2 additions & 2 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

<style>
.sb-show-main {
width: 414px;
height: 736px;
width: 416px;
height: 738px;
margin: auto;
border: 1px solid #ccc;
}
Expand Down
1 change: 1 addition & 0 deletions flow-typed/lodash.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ declare module 'lodash' {

declare module.exports: {
zip<T, U>(arr1: Array<T>, arr2: Array<U>): Array<Zip<T, U>>,
noop: (...args: any) => void,
}
}
2 changes: 2 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
import '@testing-library/jest-dom/extend-expect'

import './src/initializers/font-awesome'
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"license": "MIT",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.26",
"@fortawesome/free-brands-svg-icons": "^5.12.0",
"@fortawesome/free-regular-svg-icons": "^5.12.0",
"@fortawesome/free-solid-svg-icons": "^5.12.0",
"@fortawesome/react-fontawesome": "^0.1.8",
"axios": "^0.19.0",
"classnames": "^2.2.6",
"core-js": "^3.6.1",
Expand Down
157 changes: 157 additions & 0 deletions src/components/navbar/__tests__/navbar.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React from 'react'
import { MemoryRouter } from 'react-router-dom'
import { render, fireEvent } from '@testing-library/react'

import Navbar from '../navbar'

const renderNavbar = (items, initialEntries = ['/home']) =>
render(
<MemoryRouter initialEntries={initialEntries}>
<Navbar items={items} />
</MemoryRouter>
)

test('renders error when nav items is not divisor of 24', () => {
const items = Array(5).fill(0)
const { getByText } = renderNavbar(items)
expect(getByText('Number of items must be divisor of 24')).toBeInTheDocument()
})

test('renders active item when nav items has matched item on first render', () => {
const items = [
{
icon: 'home',
label: 'Home',
activeLabel: 'House',
key: 'home',
isActive: () => true,
},
{
icon: 'user',
label: 'About',
activeLabel: 'About me',
key: 'about',
isActive: () => false,
},
]
const { getByText, queryByText } = renderNavbar(items)
expect(queryByText('Home')).not.toBeInTheDocument()
expect(getByText('House')).toBeInTheDocument()
})

test('renders first item when nav items has multiple matched items on first render', () => {
const items = [
{
icon: 'home',
label: 'Home',
activeLabel: 'House',
key: 'home',
isActive: () => true,
},
{
icon: 'user',
label: 'About',
activeLabel: 'About me',
key: 'about',
isActive: () => true,
},
]
const { getByText, queryByText } = renderNavbar(items)
expect(getByText('House')).toBeInTheDocument()
expect(queryByText('About me')).not.toBeInTheDocument()
})

test('does not render active item when no item match', () => {
const items = [
{
icon: 'home',
label: 'Home',
activeLabel: 'House',
key: 'home',
isActive: () => false,
},
{
icon: 'user',
label: 'About',
activeLabel: 'About me',
key: 'about',
isActive: () => false,
},
]
const { getByText } = renderNavbar(items)
expect(getByText('Home')).toBeInTheDocument()
expect(getByText('About')).toBeInTheDocument()
})

test('changes active item when click', () => {
const items = [
{
icon: 'home',
label: 'Home',
activeLabel: 'House',
key: 'home',
},
{
icon: 'user',
label: 'About',
activeLabel: 'About me',
key: 'about',
},
]
const { getByText, queryByText } = renderNavbar(items)

fireEvent.click(getByText('Home'))
expect(getByText('House')).toBeInTheDocument()

fireEvent.click(getByText('About'))
expect(getByText('About me')).toBeInTheDocument()
expect(queryByText('House')).not.toBeInTheDocument()

fireEvent.click(getByText('About me'))
expect(getByText('About')).toBeInTheDocument()
})

test('render disabled item', () => {
const items = [
{
icon: 'home',
label: 'Home',
activeLabel: 'House',
key: 'home',
isDisabled: () => true,
},
{
icon: 'user',
label: 'About',
activeLabel: 'About me',
key: 'about',
},
]
const { getByText } = renderNavbar(items)
expect(getByText('Home').closest('button')).toBeDisabled()
})

test('trigger callback when click', () => {
let count = 0
const items = [
{
icon: 'home',
label: 'Home',
activeLabel: 'House',
key: 'home',
onClick: () => {
count += 1
},
},
{
icon: 'user',
label: 'About',
activeLabel: 'About me',
key: 'about',
},
]
const { getByText } = renderNavbar(items)

fireEvent.click(getByText('Home'))
expect(count).toBe(1)
})
1 change: 1 addition & 0 deletions src/components/navbar/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './navbar'
96 changes: 96 additions & 0 deletions src/components/navbar/navbar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// @flow

import React, { useState } from 'react'
import classnames from 'classnames'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { withRouter } from 'react-router-dom'

import styles from './navbar.module.scss'

type History = {
push: (path: string) => void,
}

type NavItemDefinition = {
icon: string,
activeIcon?: string,
label: string,
activeLabel?: string,
key: string,
isActive?: (url?: string) => boolean,
onClick?: (
event?: SyntheticEvent<HTMLButtonElement>,
history?: History
) => any,
isDisabled?: (url?: string) => boolean,
}

type Props = {
items: Array<NavItemDefinition>,
location: { pathname: string },
history: History,
}

const BASE_COLUMNS = 24

function Navbar({ items, location, history }: Props) {
const [selectedKey, setSelectedKey] = useState(null)

if (BASE_COLUMNS % items.length !== 0) {
return <div>Number of items must be divisor of 24</div>
}

const toggle = key => setSelectedKey(key === selectedKey ? null : key)
const columnWidth = BASE_COLUMNS / items.length

const activeItem = items.filter(
({ isActive }) => !!isActive && isActive(location.pathname)
)[0]
const activeKey = selectedKey || (activeItem && activeItem.key)

return (
<nav className={styles.navbar}>
{items.map(
({
icon,
label,
key,
activeIcon,
activeLabel,
isDisabled,
onClick,
}) => {
const isActive = key === activeKey
const displayIcon = isActive ? activeIcon || icon : icon
const displayLabel = isActive ? activeLabel || label : label

return (
<button
type="button"
className={classnames(
`pure-u-${columnWidth}-24`,
styles.navItem,
{
[styles.navItemActive]: isActive,
}
)}
key={key}
onClick={event => {
toggle(key)
onClick && onClick(event, history)
}}
disabled={!!isDisabled && isDisabled(location.pathname)}
>
<div className={styles.icon}>
<FontAwesomeIcon icon={displayIcon} size="2x" />
</div>
<div className={styles.label}>{displayLabel}</div>
</button>
)
}
)}
</nav>
)
}

export default withRouter(Navbar)
49 changes: 49 additions & 0 deletions src/components/navbar/navbar.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
$navbar-border-color: rgba(0, 0, 0, 0.1);
$nav-item-color: #6c757d;
$nav-item-active-color: #1592e6;

.navbar {
border-top: 1px solid $navbar-border-color;
height: 64px;
display: flex;
color: $nav-item-color;
}

.navItem {
height: 100%;
border: 0;
border-top: 2px solid transparent;
transition: all 0.15s ease;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0;
margin: 0;

&:focus {
outline: 0;
}

&:disabled {
opacity: 0.5;
cursor: not-allowed;
}

&:not(:disabled) {
&.navItemActive,
&:hover {
color: $nav-item-active-color;
border-top-color: $nav-item-active-color;
}
}
}

.icon {
font-size: 0.9rem;
}

.label {
margin-top: 0.15rem;
font-size: 0.8rem;
}
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import ReactDOM from 'react-dom'
import 'purecss/build/pure-min.css'
import 'highlight.js/styles/github.css'

import './initializers/font-awesome'

import './index.scss'
import App from './app'

Expand Down
4 changes: 4 additions & 0 deletions src/initializers/font-awesome.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'

library.add(fas)
3 changes: 2 additions & 1 deletion stories/0-Markdown.stories.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react'
import { MemoryRouter } from 'react-router-dom'

import 'highlight.js/styles/github.css'
import 'purecss/build/pure-min.css'

import Markdown from '../src/components/markdown'
import '../src/index.scss'
import Markdown from '../src/components/markdown'

export default {
title: 'Markdown',
Expand Down
Loading

0 comments on commit 986ac05

Please sign in to comment.