Skip to content
This repository was archived by the owner on Oct 1, 2024. It is now read-only.

Commit 54758fe

Browse files
author
george
committed
update SideNav to use react hooks
1 parent e37113e commit 54758fe

File tree

2 files changed

+83
-110
lines changed

2 files changed

+83
-110
lines changed

app/components/SideNav/SideNav.js

Lines changed: 81 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,74 @@
1-
import React, { Fragment } from "react"
1+
import React, { useState, useEffect } from "react"
22
import PropTypes from "prop-types"
33
import classNames from "classnames"
44
import throttle from "lodash/throttle"
55
import { NavLink } from "react-router-dom"
66
import Menu from "react-feather/dist/icons/menu"
77
import ChevronRight from "react-feather/dist/icons/chevron-right"
8-
import { Accordions } from "undernet"
98

9+
import { Accordions } from "undernet"
1010
import "./styles.scss"
1111

1212
const pkg = require("projectRoot/package.json")
1313
const MENU_COLLAPSE_WIDTH = 1199
1414

15-
export default class SideNav extends React.Component {
16-
constructor(props) {
17-
super(props)
18-
this.handleMenuVisibility = throttle(this.handleMenuVisibility, 50)
19-
20-
this.state = {
21-
menuIsOpen: window.innerWidth > MENU_COLLAPSE_WIDTH,
22-
}
15+
export default function SideNav(props) {
16+
const getWindowInnerWidth = () => {
17+
return window.innerWidth
2318
}
2419

25-
static propTypes = {
26-
navItems: PropTypes.arrayOf(
27-
PropTypes.shape({
28-
header: PropTypes.string,
29-
links: PropTypes.arrayOf(
30-
PropTypes.shape({
31-
name: PropTypes.string,
32-
url: PropTypes.string,
33-
})
34-
),
35-
})
36-
).isRequired,
37-
navListClasses: PropTypes.string,
38-
}
20+
const [menuIsOpen, setMenuIsOpen] = useState(getWindowInnerWidth() > MENU_COLLAPSE_WIDTH)
3921

40-
componentDidMount() {
41-
window.addEventListener("resize", this.handleMenuVisibility)
22+
let handleMenuVisibility = () => {
23+
if (getWindowInnerWidth() > MENU_COLLAPSE_WIDTH) {
24+
setMenuIsOpen(true)
25+
}
26+
}
4227

43-
const menuIsOpen = window.innerWidth > MENU_COLLAPSE_WIDTH
44-
this.setState({ menuIsOpen })
28+
handleMenuVisibility = throttle(handleMenuVisibility, 50)
4529

30+
useEffect(() => {
4631
Accordions.start()
47-
}
48-
49-
componentWillUnmount() {
50-
window.removeEventListener("resize", this.handleMenuVisibility)
51-
Accordions.stop()
52-
}
32+
window.addEventListener("resize", handleMenuVisibility)
33+
setMenuIsOpen(getWindowInnerWidth() > MENU_COLLAPSE_WIDTH)
5334

54-
componentDidUpdate(_, prevState) {
55-
if (prevState.menuIsOpen !== this.state.menuIsOpen) {
35+
return () => {
36+
window.removeEventListener("resize", handleMenuVisibility)
5637
Accordions.stop()
57-
Accordions.start()
5838
}
59-
}
39+
}, [])
6040

61-
handleCollapseClick = () => {
62-
if (window.innerWidth <= MENU_COLLAPSE_WIDTH) {
63-
this.setState({ menuIsOpen: false })
64-
}
65-
}
41+
useEffect(() => {
42+
Accordions.stop()
43+
Accordions.start()
44+
}, [menuIsOpen])
45+
46+
// handlers
6647

67-
handleMenuVisibility = () => {
68-
if (window.innerWidth > MENU_COLLAPSE_WIDTH) {
69-
this.setState({ menuIsOpen: true })
48+
const handleCollapseClick = () => {
49+
if (getWindowInnerWidth() <= MENU_COLLAPSE_WIDTH) {
50+
setMenuIsOpen(false)
7051
}
7152
}
7253

73-
handleMenuToggleClick = event => {
54+
const handleMenuToggleClick = event => {
7455
event.preventDefault()
75-
this.setState({ menuIsOpen: !this.state.menuIsOpen })
56+
setMenuIsOpen(!menuIsOpen)
7657
}
7758

78-
get buttonClasses() {
59+
const buttonClasses = () => {
7960
return classNames("is-justified-center is-aligned-center is-flex is-hidden-xlarge", {
80-
"rotate-180": !this.state.menuIsOpen,
61+
"rotate-180": !menuIsOpen,
8162
})
8263
}
8364

84-
get menuClasses() {
65+
const menuClasses = () => {
8566
return classNames("row side-nav-menu accordion has-padding-3", {
86-
"is-hidden": !this.state.menuIsOpen,
67+
"is-hidden": !menuIsOpen,
8768
})
8869
}
8970

90-
accordionIsActive(items) {
71+
const accordionIsActive = items => {
9172
let isActive = false
9273

9374
items.forEach(item => {
@@ -99,14 +80,14 @@ export default class SideNav extends React.Component {
9980
return isActive
10081
}
10182

102-
renderAccordionChildLink = item => {
83+
const renderAccordionChildLink = item => {
10384
return (
10485
<li key={item.name} role="none">
10586
<NavLink
10687
role="listitem"
10788
className="side-nav-link-item has-black-text is-flex is-aligned-center"
10889
activeClassName="active"
109-
onClick={this.handleCollapseClick}
90+
onClick={handleCollapseClick}
11091
to={item.url}
11192
>
11293
{item.name} <ChevronRight size={16} role="presentation" focusable="false" />
@@ -115,11 +96,11 @@ export default class SideNav extends React.Component {
11596
)
11697
}
11798

118-
renderAccordionRow(section, index) {
119-
const listItems = section.links.map(this.renderAccordionChildLink)
99+
const renderAccordionRow = (section, index) => {
100+
const listItems = section.links.map(renderAccordionChildLink)
120101

121102
return (
122-
<Fragment key={section.header}>
103+
<React.Fragment key={section.header}>
123104
<h4 className="paragraph">
124105
<button
125106
id={`nav-acc-button${index}`}
@@ -133,53 +114,62 @@ export default class SideNav extends React.Component {
133114
<ul className="accordion-content" id={`nav-acc-content${index}`}>
134115
{listItems}
135116
</ul>
136-
</Fragment>
117+
</React.Fragment>
137118
)
138119
}
139120

140-
renderNavAccordion() {
141-
return this.props.navItems.map((section, i) => {
121+
const renderNavAccordion = () => {
122+
return props.navItems.map((section, i) => {
142123
return (
143124
<div
144-
data-visible={this.accordionIsActive(section.links) ? "true" : "false"}
125+
data-visible={accordionIsActive(section.links) ? "true" : "false"}
145126
data-accordion-row={`nav-acc-content${i}`}
146-
className={classNames("accordion-row", this.props.navListClasses)}
127+
className={classNames("accordion-row", props.navListClasses)}
147128
key={section.links[0].url}
148129
>
149-
{this.renderAccordionRow(section, i)}
130+
{renderAccordionRow(section, i)}
150131
</div>
151132
)
152133
})
153134
}
154135

155-
render() {
156-
return (
157-
<div className="xsmall-12 xlarge-2 columns has-no-padding" id="side-nav">
158-
<div className="fluid grid side-nav-wrapper">
159-
<div className="row is-flex is-hidden-xlarge side-nav-expand">
160-
<button
161-
onClick={this.handleMenuToggleClick}
162-
className={this.buttonClasses}
163-
aria-controls="side-nav-wrapper"
164-
aria-expanded={this.state.menuIsOpen}
165-
>
166-
<Menu size={20} role="presentation" focusable="false" />{" "}
167-
<span className="has-black-text">Explore</span>
168-
</button>
169-
</div>
170-
171-
<nav
172-
data-accordion="side-nav-accordion"
173-
className={this.menuClasses}
174-
id="side-nav-wrapper"
136+
return (
137+
<div className="xsmall-12 xlarge-2 columns has-no-padding" id="side-nav">
138+
<div className="fluid grid side-nav-wrapper">
139+
<div className="row is-flex is-hidden-xlarge side-nav-expand">
140+
<button
141+
onClick={handleMenuToggleClick}
142+
className={buttonClasses()}
143+
aria-controls="side-nav-wrapper"
144+
aria-expanded={menuIsOpen}
175145
>
176-
<p className="version-text has-no-padding has-gray800-text xsmall-12 columns">
177-
Version {pkg.version}
178-
</p>
179-
{this.renderNavAccordion()}
180-
</nav>
146+
<Menu size={20} role="presentation" focusable="false" />{" "}
147+
<span className="has-black-text">Explore</span>
148+
</button>
181149
</div>
150+
151+
<nav data-accordion="side-nav-accordion" className={menuClasses()} id="side-nav-wrapper">
152+
<p className="version-text has-no-padding has-gray800-text xsmall-12 columns">
153+
Version {pkg.version}
154+
</p>
155+
{renderNavAccordion()}
156+
</nav>
182157
</div>
183-
)
184-
}
158+
</div>
159+
)
160+
}
161+
162+
SideNav.propTypes = {
163+
navItems: PropTypes.arrayOf(
164+
PropTypes.shape({
165+
header: PropTypes.string,
166+
links: PropTypes.arrayOf(
167+
PropTypes.shape({
168+
name: PropTypes.string,
169+
url: PropTypes.string,
170+
})
171+
),
172+
})
173+
).isRequired,
174+
navListClasses: PropTypes.string,
185175
}

app/components/SideNav/__tests__/SideNav.spec.js

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import { Accordions } from "undernet"
55

66
jest.mock("react-feather/dist/icons/chevron-right", () => global.simpleMock("ChevronRight"))
77
jest.mock("react-feather/dist/icons/menu", () => global.simpleMock("Menu"))
8-
jest.mock("projectRoot/package.json", () => ({
9-
version: "9.9.9",
10-
}))
8+
jest.mock("projectRoot/package.json", () => ({ version: "9.9.9" }))
119

1210
Accordions.start = jest.fn()
1311
Accordions.stop = jest.fn()
@@ -35,7 +33,7 @@ function updatePageWidth(width) {
3533
window.dispatchEvent(new Event("resize"))
3634
}
3735

38-
describe.only("<SideNav />", () => {
36+
describe("<SideNav />", () => {
3937
describe("#render", () => {
4038
it("renders", () => {
4139
// Given
@@ -45,21 +43,6 @@ describe.only("<SideNav />", () => {
4543
})
4644
})
4745

48-
describe("#state", () => {
49-
beforeEach(() => {
50-
const wrapper = mountComponent()
51-
wrapper.find("SideNav").setState({ menuIsOpen: true })
52-
})
53-
54-
it("calls Accordions.stop if state.menuIsOpen changes", () => {
55-
expect(Accordions.stop).toHaveBeenCalled()
56-
})
57-
58-
it("calls Accordions.start if state.menuIsOpen changes", () => {
59-
expect(Accordions.start).toHaveBeenCalled()
60-
})
61-
})
62-
6346
describe("#handleMenuVisibility", () => {
6447
afterEach(() => {
6548
updatePageWidth(DEFAULT_WIDTH)

0 commit comments

Comments
 (0)