feat: add toggle nav (#36956)
* feat: re-add toggle menu * Update client/src/components/Header/components/universalNav.css Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * Update client/src/components/Header/components/universalNav.css Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * Update client/src/components/Header/components/universalNav.css Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * fix: fix lint error
This commit is contained in:
committed by
mrugesh
parent
2066ed674b
commit
f9a112b43e
@ -2,19 +2,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
import ShallowRenderer from 'react-test-renderer/shallow';
|
||||||
import TestRenderer from 'react-test-renderer';
|
import TestRenderer from 'react-test-renderer';
|
||||||
|
import { UniversalNav } from './components/UniversalNav';
|
||||||
import Header from './';
|
|
||||||
import NavLinks from './components/NavLinks';
|
import NavLinks from './components/NavLinks';
|
||||||
|
|
||||||
describe('<Header />', () => {
|
describe('<UniversalNav />', () => {
|
||||||
it('renders to the DOM', () => {
|
it('renders to the DOM', () => {
|
||||||
const shallow = new ShallowRenderer();
|
const shallow = new ShallowRenderer();
|
||||||
shallow.render(<Header />);
|
shallow.render(<UniversalNav {...UniversalNavProps} />);
|
||||||
const result = shallow.getRenderOutput();
|
const result = shallow.getRenderOutput();
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('<NavLinks />', () => {
|
describe('<NavLinks />', () => {
|
||||||
const root = TestRenderer.create(<NavLinks />).root;
|
const root = TestRenderer.create(<NavLinks />).root;
|
||||||
const aTags = root.findAllByType('a');
|
const aTags = root.findAllByType('a');
|
||||||
@ -25,17 +23,23 @@ describe('<NavLinks />', () => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const expectedLinks = ['/', '/portfolio'];
|
const expectedLinks = ['/learn', '/', '/portfolio'];
|
||||||
|
|
||||||
it('renders to the DOM', () => {
|
it('renders to the DOM', () => {
|
||||||
expect(root).toBeTruthy();
|
expect(root).toBeTruthy();
|
||||||
});
|
});
|
||||||
it('has 3 a tags', () => {
|
it('has 3 links', () => {
|
||||||
expect(aTags.length === 3).toBeTruthy();
|
expect(aTags.length === 3).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has link to portfolio', () => {
|
it('has links to learn, main, and portfolio', () => {
|
||||||
// checks if all links in expected links exist in links
|
// checks if all links in expected links exist in links
|
||||||
expect(expectedLinks.every(elem => links.indexOf(elem) > -1)).toBeTruthy();
|
expect(expectedLinks.every(elem => links.indexOf(elem) > -1)).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const UniversalNavProps = {
|
||||||
|
displayMenu: false,
|
||||||
|
menuButtonRef: {},
|
||||||
|
toggleDisplayMenu: function() {}
|
||||||
|
};
|
||||||
|
26
client/src/components/Header/components/MenuButton.js
Normal file
26
client/src/components/Header/components/MenuButton.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const MenuButton = React.forwardRef((props, ref) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-expanded={props.displayMenu}
|
||||||
|
className={
|
||||||
|
'toggle-button-nav' + (props.displayMenu ? ' reverse-toggle-color' : '')
|
||||||
|
}
|
||||||
|
onClick={props.onClick}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
Menu
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
MenuButton.displayName = 'MenuButton';
|
||||||
|
MenuButton.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
displayMenu: PropTypes.bool.isRequired,
|
||||||
|
onClick: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuButton;
|
@ -1,16 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from '../../helpers';
|
import { Link } from '../../helpers';
|
||||||
|
|
||||||
export function NavLinks() {
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
displayMenu: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
function NavLinks({ displayMenu }) {
|
||||||
return (
|
return (
|
||||||
<div className='main-nav-group'>
|
<div className='main-nav-group'>
|
||||||
<ul className={'nav-list display-flex'} role='menu'>
|
<ul
|
||||||
<li className='nav-theme' role='menuitem'>
|
className={'nav-list' + (displayMenu ? ' display-flex' : '')}
|
||||||
<Link to='/learn'>Projects</Link>
|
role='menu'
|
||||||
</li>
|
>
|
||||||
<li className='nav-theme' role='menuitem'>
|
<li className='nav-theme' role='menuitem'>
|
||||||
<Link to='/'>Light</Link>
|
<Link to='/'>Light</Link>
|
||||||
</li>
|
</li>
|
||||||
|
<li className='nav-projects' role='menuitem'>
|
||||||
|
<Link to='/learn'>Projects</Link>
|
||||||
|
</li>
|
||||||
<li className='nav-portfolio' role='menuitem'>
|
<li className='nav-portfolio' role='menuitem'>
|
||||||
<Link to='/portfolio'>Portfolio</Link>
|
<Link to='/portfolio'>Portfolio</Link>
|
||||||
</li>
|
</li>
|
||||||
@ -19,5 +28,6 @@ export function NavLinks() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavLinks.propTypes = propTypes;
|
||||||
NavLinks.displayName = 'NavLinks';
|
NavLinks.displayName = 'NavLinks';
|
||||||
export default NavLinks;
|
export default NavLinks;
|
||||||
|
@ -1,27 +1,48 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { Link } from '../../helpers';
|
import { Link } from '../../helpers';
|
||||||
import NavLogo from './NavLogo';
|
import NavLogo from './NavLogo';
|
||||||
import SearchBar from '../../search/searchBar/SearchBar';
|
import SearchBar from '../../search/searchBar/SearchBar';
|
||||||
|
import MenuButton from './MenuButton';
|
||||||
import NavLinks from './NavLinks';
|
import NavLinks from './NavLinks';
|
||||||
import './universalNav.css';
|
import './universalNav.css';
|
||||||
|
|
||||||
export function UniversalNav() {
|
export const UniversalNav = React.forwardRef(
|
||||||
return (
|
({ displayMenu, toggleDisplayMenu }, ref) => (
|
||||||
<nav className={'universal-nav nav-padding'} id='universal-nav'>
|
<nav
|
||||||
<div className={'universal-nav-left'}>
|
className={
|
||||||
|
'universal-nav nav-padding' + (displayMenu ? ' expand-nav' : '')
|
||||||
|
}
|
||||||
|
id='universal-nav'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={'universal-nav-left' + (displayMenu ? ' display-flex' : '')}
|
||||||
|
>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
</div>
|
</div>
|
||||||
<div className='universal-nav-middle'>
|
<div className='universal-nav-middle'>
|
||||||
<Link className='universal-nav-logo' to='/'>
|
<Link id='universal-nav-logo' to='/'>
|
||||||
<NavLogo />
|
<NavLogo />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className='universal-nav-right main-nav'>
|
<div className='universal-nav-right main-nav'>
|
||||||
<NavLinks />
|
<NavLinks displayMenu={displayMenu} />
|
||||||
</div>
|
</div>
|
||||||
|
<MenuButton
|
||||||
|
displayMenu={displayMenu}
|
||||||
|
onClick={toggleDisplayMenu}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
UniversalNav.displayName = 'UniversalNav';
|
UniversalNav.displayName = 'UniversalNav';
|
||||||
export default UniversalNav;
|
export default UniversalNav;
|
||||||
|
|
||||||
|
UniversalNav.propTypes = {
|
||||||
|
displayMenu: PropTypes.bool,
|
||||||
|
menuButtonRef: PropTypes.object,
|
||||||
|
toggleDisplayMenu: PropTypes.func
|
||||||
|
};
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-family: 'Lato', sans-serif;
|
font-family: 'Lato', sans-serif;
|
||||||
height: calc(2 * var(--header-height));
|
height: var(--header-height);
|
||||||
background: var(--theme-color);
|
background: var(--theme-color);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@ -31,28 +31,25 @@
|
|||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.4px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
-ms-overflow-scrolling: touch;
|
-ms-overflow-scrolling: touch;
|
||||||
margin-right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.universal-nav-logo {
|
#universal-nav-logo {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
margin: 0px;
|
||||||
color: var(--gray-00);
|
color: var(--gray-00);
|
||||||
font-size: 1.7rem;
|
font-size: 1.7rem;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: -0.5px;
|
letter-spacing: -0.5px;
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: absolute;
|
|
||||||
left: 15px;
|
|
||||||
top: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.universal-nav-logo:hover {
|
#universal-nav-logo:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: var(--theme-color);
|
background-color: var(--theme-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.universal-nav-logo img {
|
#universal-nav-logo img {
|
||||||
display: block;
|
display: block;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
@ -67,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-list {
|
.nav-list {
|
||||||
height: var(--header-height);
|
height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-list li {
|
.nav-list li {
|
||||||
@ -83,7 +80,6 @@
|
|||||||
color: var(--gray-00);
|
color: var(--gray-00);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
height: var(--header-height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-list li:hover {
|
.nav-list li:hover {
|
||||||
@ -103,43 +99,115 @@
|
|||||||
height: 38px;
|
height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 380px) {
|
.toggle-button-nav {
|
||||||
.universal-nav {
|
display: none;
|
||||||
padding: 0px 5px;
|
padding: 2px 14px 2px;
|
||||||
}
|
border: 1px solid var(--gray-00);
|
||||||
.universal-nav-logo {
|
font-family: 'lato', sans-serif;
|
||||||
left: 5px;
|
font-size: 18px;
|
||||||
}
|
color: var(--gray-00);
|
||||||
|
outline: none;
|
||||||
|
background-color: var(--theme-color);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 4px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 265px) {
|
||||||
.nav-list li a {
|
.nav-list li a {
|
||||||
padding: 8px 5px;
|
width: 50vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 700px) {
|
@media (max-width: 799px) {
|
||||||
.universal-nav-logo {
|
.site-header {
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.universal-nav-middle {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.universal-nav-left {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--theme-color);
|
||||||
|
top: calc(var(--header-height) * 2);
|
||||||
|
right: 0px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100vw;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button-nav {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-nav {
|
||||||
|
min-height: calc(3 * var(--header-height));
|
||||||
|
}
|
||||||
|
.reverse-toggle-color {
|
||||||
|
background-color: var(--gray-00);
|
||||||
|
color: var(--theme-color);
|
||||||
|
}
|
||||||
|
.universal-nav-left form {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.fcc_searchBar .ais-SearchBox-form {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: var(--header-height);
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
#universal-nav-logo {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
left: 17px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.universal-nav-middle {
|
||||||
|
flex: 1 0 30%;
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
.universal-nav-left {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 35%;
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
#universal-nav-logo {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
position: static;
|
}
|
||||||
left: auto;
|
.universal-nav-right {
|
||||||
top: auto;
|
flex: 1 0 35%;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
.main-nav-group {
|
.main-nav-group {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1500px) {
|
||||||
.universal-nav-middle {
|
.universal-nav-middle {
|
||||||
flex: 1 0 33%;
|
flex: 1 0 33%;
|
||||||
margin-right: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
}
|
||||||
.universal-nav-left {
|
.universal-nav-left {
|
||||||
flex: 1 0 33%;
|
flex: 1 0 33%;
|
||||||
margin-left: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.universal-nav-right {
|
.universal-nav-right {
|
||||||
flex: 1 0 33%;
|
flex: 1 0 33%;
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
.universal-nav {
|
|
||||||
height: var(--header-height);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,17 +5,56 @@ import UniversalNav from './components/UniversalNav';
|
|||||||
|
|
||||||
import './header.css';
|
import './header.css';
|
||||||
|
|
||||||
export function Header() {
|
export class Header extends React.Component {
|
||||||
return (
|
constructor(props) {
|
||||||
<>
|
super(props);
|
||||||
<Helmet>
|
this.state = {
|
||||||
<style>{':root{--header-height: 38px}'}</style>
|
displayMenu: false
|
||||||
</Helmet>
|
};
|
||||||
<header>
|
this.menuButtonRef = React.createRef();
|
||||||
<UniversalNav />
|
this.handleClickOutside = this.handleClickOutside.bind(this);
|
||||||
</header>
|
this.toggleDisplayMenu = this.toggleDisplayMenu.bind(this);
|
||||||
</>
|
}
|
||||||
);
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('click', this.handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('click', this.handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickOutside(event) {
|
||||||
|
if (
|
||||||
|
this.state.displayMenu &&
|
||||||
|
this.menuButtonRef.current &&
|
||||||
|
!this.menuButtonRef.current.contains(event.target) &&
|
||||||
|
event.target.id !== 'fcc_instantsearch'
|
||||||
|
) {
|
||||||
|
this.toggleDisplayMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDisplayMenu() {
|
||||||
|
this.setState(({ displayMenu }) => ({ displayMenu: !displayMenu }));
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const { displayMenu } = this.state;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<style>{':root{--header-height: 38px}'}</style>
|
||||||
|
</Helmet>
|
||||||
|
<header>
|
||||||
|
<UniversalNav
|
||||||
|
displayMenu={displayMenu}
|
||||||
|
ref={this.menuButtonRef}
|
||||||
|
toggleDisplayMenu={this.toggleDisplayMenu}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Header.displayName = 'Header';
|
Header.displayName = 'Header';
|
||||||
|
@ -29,6 +29,12 @@
|
|||||||
color: var(--gray-00);
|
color: var(--gray-00);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fcc_searchBar .ais-Hits {
|
||||||
|
top: 71px;
|
||||||
|
width: calc(100vw - 30px);
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.fcc_searchBar .ais-SearchBox-form {
|
.fcc_searchBar .ais-SearchBox-form {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -37,12 +43,6 @@
|
|||||||
right: 5px;
|
right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fcc_searchBar .ais-Hits {
|
|
||||||
top: 71px;
|
|
||||||
width: calc(100vw - 10px);
|
|
||||||
left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* hits */
|
/* hits */
|
||||||
.fcc_searchBar .ais-Highlight-highlighted {
|
.fcc_searchBar .ais-Highlight-highlighted {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@ -103,23 +103,18 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 380px) {
|
.ais-SearchBox-input {
|
||||||
.fcc_searchBar .ais-Hits {
|
width: calc(100vw - 30px);
|
||||||
width: calc(100vw - 30px);
|
|
||||||
left: 15px;
|
|
||||||
}
|
|
||||||
.ais-SearchBox-input {
|
|
||||||
width: calc(100vw - 30px);
|
|
||||||
}
|
|
||||||
.fcc_searchBar .ais-SearchBox-form {
|
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
|
||||||
top: var(--header-height);
|
|
||||||
right: 15px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 700px) {
|
.fcc_searchBar .ais-SearchBox-form {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: var(--header-height);
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
.ais-SearchBox-input {
|
.ais-SearchBox-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
@ -130,7 +125,8 @@
|
|||||||
}
|
}
|
||||||
.fcc_searchBar .ais-Hits {
|
.fcc_searchBar .ais-Hits {
|
||||||
top: auto;
|
top: auto;
|
||||||
width: calc(80vw - 20px);
|
width: calc(100% - 20px);
|
||||||
|
max-width: 500px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
}
|
}
|
||||||
.fcc_searchBar .ais-SearchBox-form {
|
.fcc_searchBar .ais-SearchBox-form {
|
||||||
@ -143,8 +139,3 @@
|
|||||||
left: 0.85rem;
|
left: 0.85rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 1100px) {
|
|
||||||
.fcc_searchBar .ais-Hits {
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user