fix(client): make top navigation menu replaceable (#35943)

* fix(client): Stop using react-responsive and use media queries to display menu

* Change guide to show menu

* DRYed out a bit

* Restore main, top-right nav to guide

* fix: Separate guide and top menu state

* Update client/src/components/Header/index.js

Co-Authored-By: Valeriy <ValeraS@users.noreply.github.com>

* Update client/src/components/Header/index.js

Co-Authored-By: Valeriy <ValeraS@users.noreply.github.com>

* Update client/src/components/Header/index.js

Co-Authored-By: Valeriy <ValeraS@users.noreply.github.com>

* Update client/src/components/Header/index.js

Co-Authored-By: Valeriy <ValeraS@users.noreply.github.com>

* fix: Refactor menu button and links

* feat(client): make top navigation menu replaceable

* fix: Refactor nav menu logic out of Header

* fix(client): use default nav menu in header and use landingPage props instead of disableSettings
This commit is contained in:
Oliver Eyton-Williams 2019-05-26 18:36:23 +01:00 committed by Valeriy
parent 02427ad982
commit 17e112d25e
12 changed files with 310 additions and 265 deletions

View File

@ -10,6 +10,7 @@ import {
DefaultLayout,
GuideLayout
} from './src/components/layouts';
import GuideNavMenu from './src/components/layouts/components/guide/NavMenu';
const store = createStore();
@ -41,7 +42,7 @@ export const wrapPageElement = ({ element, props }) => {
}
if (/^\/guide(\/.*)*/.test(pathname)) {
return (
<DefaultLayout disableMenuButtonBehavior={true} mediaBreakpoint='991px'>
<DefaultLayout navigationMenu={<GuideNavMenu />}>
<GuideLayout>{element}</GuideLayout>
</DefaultLayout>
);

View File

@ -5,12 +5,9 @@ import { Provider } from 'react-redux';
import headComponents from './src/head';
import { createStore } from './src/redux/createStore';
import { wrapPageElement } from './gatsby-browser';
import {
CertificationLayout,
DefaultLayout,
GuideLayout
} from './src/components/layouts';
export { wrapPageElement };
const store = createStore();
@ -22,39 +19,6 @@ wrapRootElement.propTypes = {
element: PropTypes.any
};
export const wrapPageElement = ({ element, props }) => {
const {
location: { pathname }
} = props;
if (pathname === '/') {
return (
<DefaultLayout disableSettings={true} landingPage={true}>
{element}
</DefaultLayout>
);
}
if (/^\/certification(\/.*)*/.test(pathname)) {
return <CertificationLayout>{element}</CertificationLayout>;
}
if (/^\/guide(\/.*)*/.test(pathname)) {
return (
<DefaultLayout onGuide={true}>
<GuideLayout>{element}</GuideLayout>
</DefaultLayout>
);
}
if (/^\/learn(\/.*)*/.test(pathname)) {
return <DefaultLayout showFooter={false}>{element}</DefaultLayout>;
}
return <DefaultLayout>{element}</DefaultLayout>;
};
wrapPageElement.propTypes = {
element: PropTypes.any,
location: PropTypes.objectOf({ pathname: PropTypes.string }),
props: PropTypes.any
};
export const onRenderBody = ({ setHeadComponents, setPostBodyComponents }) => {
setHeadComponents([...headComponents]);
setPostBodyComponents(

View File

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import './menuButton.css';
const MenuButton = React.forwardRef((props, ref) => (
<button
aria-expanded={props.displayMenu}
className={
'menu-button' +
(props.displayMenu ? ' menu-button-open' : '') +
' ' +
(props.className ? props.className : 'top-menu-button')
}
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;

View File

@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '../../helpers';
import UserState from '../components/UserState';
import './menuLinks.css';
function MenuLinks(props) {
return (
<ul className={props.className} id='top-right-nav'>
<li>
<Link className='top-right-nav-link' to='/learn'>
Learn
</Link>
</li>
<li>
<Link className='top-right-nav-link' external={true} to='/forum'>
Forum
</Link>
</li>
<li>
<Link className='top-right-nav-link' external={true} to='/news'>
News
</Link>
</li>
<li>
<UserState disableSettings={props.disableSettings} />
</li>
</ul>
);
}
MenuLinks.displayName = 'MenuLinks';
MenuLinks.propTypes = {
className: PropTypes.string,
disableSettings: PropTypes.bool
};
export default MenuLinks;

View File

@ -0,0 +1,65 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import MenuButton from './MenuButton';
import MenuLinks from './MenuLinks';
class NavigationMenu extends Component {
constructor(props) {
super(props);
this.state = {
displayMenu: false
};
this.menuButtonRef = React.createRef();
this.handleClickOutside = this.handleClickOutside.bind(this);
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)
) {
this.toggleDisplayMenu();
}
}
toggleDisplayMenu() {
this.setState(({ displayMenu }) => ({ displayMenu: !displayMenu }));
}
render() {
const { disableSettings } = this.props;
const { displayMenu } = this.state;
return (
<>
<MenuButton
displayMenu={displayMenu}
onClick={this.toggleDisplayMenu}
ref={this.menuButtonRef}
/>
<MenuLinks
className={'top-nav' + (displayMenu ? ' top-nav-expanded' : '')}
disableSettings={disableSettings}
/>
</>
);
}
}
NavigationMenu.displayName = 'NavigationMenu';
NavigationMenu.propTypes = {
disableSettings: PropTypes.bool
};
export default NavigationMenu;

View File

@ -0,0 +1,26 @@
#top-nav .menu-button {
color: white;
background-color: transparent;
margin: 0 20px 0 12px;
padding: 2px 14px;
border: 1px solid #fff;
border-radius: 3px;
}
#top-nav .menu-button-open {
background: white;
color: #006400;
}
@media (min-width: 735px) {
#top-nav .top-menu-button {
display: none;
}
}
@media (max-width: 420px) {
#top-nav .menu-button {
margin: 0 10px 0 4px;
}
}

View File

@ -0,0 +1,62 @@
#top-right-nav {
display: flex;
margin: 0;
list-style: none;
justify-content: space-between;
align-items: center;
background-color: #006400;
}
#top-right-nav li {
margin: 0;
}
.top-right-nav-link {
max-height: var(--header-height);
color: #fff;
font-size: 18px;
padding: 8px 15px;
}
.top-right-nav-link:hover,
.top-right-nav-link:focus,
.top-right-nav-link:active {
background-color: #fff;
color: #006400;
text-decoration: none;
}
.user-state-spinner {
height: var(--header-height);
padding: 0 12px;
}
.user-state-spinner > div {
animation-duration: 1.5s !important;
}
#top-right-nav .signup-btn,
#top-right-nav .settings-link {
margin: 0 15px;
}
#top-right-nav .user-avatar {
max-height: calc(var(--header-height) - 4px);
}
@media (max-width: 734px) {
#top-right-nav {
position: absolute;
top: var(--header-height);
flex-direction: column;
width: 100vw;
height: min-content;
min-height: 160px;
padding: 10px 0;
display: none;
}
#top-right-nav.top-nav-expanded {
display: flex;
}
}

View File

@ -26,49 +26,6 @@ header {
align-items: center;
}
#top-nav .nav-logo {
max-height: 25px;
min-width: 35px;
margin: 0 5px;
}
#top-right-nav {
display: flex;
margin: 0;
list-style: none;
justify-content: space-between;
align-items: center;
background-color: #006400;
}
#top-right-nav li {
margin: 0;
}
.top-right-nav-link {
max-height: var(--header-height);
color: #fff;
font-size: 18px;
padding: 8px 15px;
}
.top-right-nav-link:hover,
.top-right-nav-link:focus,
.top-right-nav-link:active {
background-color: #fff;
color: #006400;
text-decoration: none;
}
.user-state-spinner {
height: var(--header-height);
padding: 0 12px;
}
.user-state-spinner > div {
animation-duration: 1.5s !important;
}
/* Search bar */
.fcc_searchBar {
flex-grow: 1;
@ -127,6 +84,12 @@ header {
/* Navbar logo */
#top-nav .nav-logo {
max-height: 25px;
min-width: 35px;
margin: 0 5px;
}
.logoContainer {
margin-right: 10px;
}
@ -138,44 +101,11 @@ header {
}
}
#top-nav .menu-button {
color: white;
background-color: transparent;
margin: 0 20px 0 12px;
padding: 2px 14px;
border: 1px solid #fff;
border-radius: 3px;
}
#top-nav .menu-button-open {
background: white;
color: #006400;
}
#top-right-nav .signup-btn,
#top-right-nav .settings-link {
margin: 0 15px;
}
#top-right-nav .user-avatar {
max-height: calc(var(--header-height) - 4px);
}
@media (max-width: 734px) {
#top-nav {
padding: 0;
}
#top-right-nav {
position: absolute;
top: var(--header-height);
flex-direction: column;
width: 100vw;
height: min-content;
min-height: 160px;
padding: 10px 0;
}
#top-nav .nav-logo {
margin: 0 0 0 10px;
}
@ -185,14 +115,4 @@ header {
#top-nav .nav-logo {
margin: 0 0 0 5px;
}
#top-nav .menu-button {
margin: 0 10px 0 4px;
}
.ais-Hits {
background-color: #fff;
left: 0.5em;
top: 2.4em;
}
}

View File

@ -1,145 +1,37 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { createSelector } from 'reselect';
import Media from 'react-responsive';
import FCCSearch from 'react-freecodecamp-search';
import NavigationMenu from './components/NavMenu';
import NavLogo from './components/NavLogo';
import UserState from './components/UserState';
import { Link } from '../helpers';
import './header.css';
import {
toggleDisplayMenu,
displayMenuSelector
} from '../layouts/components/guide/redux';
const mapStateToProps = createSelector(
displayMenuSelector,
displayMenu => ({
displayMenu
})
);
const mapDispatchToProps = dispatch =>
bindActionCreators({ toggleDisplayMenu }, dispatch);
const propTypes = {
disableMenuButtonBehavior: PropTypes.bool,
disableSettings: PropTypes.bool,
displayMenu: PropTypes.bool,
mediaBreakpoint: PropTypes.string.isRequired,
toggleDisplayMenu: PropTypes.func.isRequired
navigationMenu: PropTypes.element
};
class Header extends Component {
constructor(props) {
super(props);
this.menuButtonRef = React.createRef();
}
componentDidMount() {
document.addEventListener('click', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside);
}
handleClickOutside = event => {
if (
!this.props.disableMenuButtonBehavior &&
this.props.displayMenu &&
this.menuButtonRef.current &&
!this.menuButtonRef.current.contains(event.target)
) {
this.props.toggleDisplayMenu();
}
};
handleMediaChange = matches => {
if (!matches && this.props.displayMenu) {
this.props.toggleDisplayMenu();
}
};
render() {
const {
disableMenuButtonBehavior,
disableSettings,
displayMenu,
mediaBreakpoint,
toggleDisplayMenu
} = this.props;
return (
<header>
<nav id='top-nav'>
<Link className='home-link' to='/'>
<NavLogo />
</Link>
{disableSettings ? null : <FCCSearch />}
<Media maxWidth={mediaBreakpoint} onChange={this.handleMediaChange}>
{matches => [
matches && (
<button
aria-expanded={displayMenu}
className={
'menu-button' + (displayMenu ? ' menu-button-open' : '')
}
key='menu-button'
onClick={toggleDisplayMenu}
ref={this.menuButtonRef}
>
Menu
</button>
),
(!matches || (displayMenu && !disableMenuButtonBehavior)) && (
<ul id='top-right-nav' key='top-right-nav'>
<li>
<Link className='top-right-nav-link' to='/learn'>
Learn
</Link>
</li>
<li>
<Link
className='top-right-nav-link'
external={true}
to='/forum'
>
Forum
</Link>
</li>
<li>
<Link
className='top-right-nav-link'
external={true}
to='/news'
>
News
</Link>
</li>
<li>
<UserState disableSettings={disableSettings} />
</li>
</ul>
)
]}
</Media>
</nav>
</header>
);
}
function Header(props) {
const { disableSettings, navigationMenu } = props;
return (
<header>
<nav id='top-nav'>
<Link className='home-link' to='/'>
<NavLogo />
</Link>
{disableSettings ? null : <FCCSearch />}
{navigationMenu ? (
navigationMenu
) : (
<NavigationMenu disableSettings={disableSettings} />
)}
</nav>
</header>
);
}
Header.propTypes = propTypes;
Header.defaultProps = {
mediaBreakpoint: '734px'
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Header);
export default Header;

View File

@ -56,8 +56,6 @@ const metaKeywords = [
const propTypes = {
children: PropTypes.node.isRequired,
disableMenuButtonBehavior: PropTypes.bool,
disableSettings: PropTypes.bool,
fetchUser: PropTypes.func.isRequired,
flashMessages: PropTypes.arrayOf(
PropTypes.shape({
@ -70,7 +68,7 @@ const propTypes = {
isOnline: PropTypes.bool.isRequired,
isSignedIn: PropTypes.bool,
landingPage: PropTypes.bool,
mediaBreakpoint: PropTypes.string,
navigationMenu: PropTypes.element.isRequired,
onlineStatusChange: PropTypes.func.isRequired,
removeFlashMessage: PropTypes.func.isRequired,
showFooter: PropTypes.bool
@ -136,14 +134,12 @@ class DefaultLayout extends Component {
render() {
const {
children,
disableSettings,
hasMessages,
flashMessages = [],
removeFlashMessage,
landingPage,
showFooter = true,
mediaBreakpoint,
disableMenuButtonBehavior,
navigationMenu,
isOnline,
isSignedIn
} = this.props;
@ -162,11 +158,7 @@ class DefaultLayout extends Component {
>
<style>{fontawesome.dom.css()}</style>
</Helmet>
<Header
disableMenuButtonBehavior={disableMenuButtonBehavior}
disableSettings={disableSettings}
mediaBreakpoint={mediaBreakpoint}
/>
<Header disableSettings={landingPage} navigationMenu={navigationMenu} />
<div className={`default-layout ${landingPage ? 'landing-page' : ''}`}>
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
{hasMessages ? (

View File

@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { createSelector } from 'reselect';
import MenuButton from '../../../../components/Header/components/MenuButton';
import MenuLinks from '../../../../components/Header/components/MenuLinks';
import { toggleDisplayMenu, displayMenuSelector } from './redux';
const mapStateToProps = createSelector(
displayMenuSelector,
displayMenu => ({
displayMenu
})
);
const mapDispatchToProps = dispatch =>
bindActionCreators({ toggleDisplayMenu }, dispatch);
function GuideNavigationMenu(props) {
const { displayMenu, toggleDisplayMenu } = props;
return (
<>
<MenuButton
className={'guide-menu-button'}
displayMenu={displayMenu}
onClick={toggleDisplayMenu}
/>
<MenuLinks className={'guide-top-nav'} />
</>
);
}
GuideNavigationMenu.displayName = 'GuideNavigationMenu';
GuideNavigationMenu.propTypes = {
displayMenu: PropTypes.bool.isRequired,
toggleDisplayMenu: PropTypes.func.isRequired
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(GuideNavigationMenu);

View File

@ -124,6 +124,15 @@
.content {
height: auto;
}
#top-right-nav.guide-top-nav {
display: none;
}
}
@media (min-width: 993px) {
#top-nav .guide-menu-button {
display: none;
}
}
.content,