Skip to content

Commit

Permalink
Implement Header core organism and base layout component
Browse files Browse the repository at this point in the history
> Layout

The header uses a CSS flexbox layout consisting of two container
components with the maximum of flexible space between both. The
containers contain the branding component and the navigation like
documented in the sections below.

> Navigation

To allows users to simply navigate around the site, the component
provides the quickly accessible navigation. It is placed in the
right-sided container. As of now the following link items are included:

- "Ports" - links to `/ports`, the landing page for all Nord port
  projects.
- "Docs" - links to `/docs`, the landing page for Nord's documentation.
- "Blog" - links to `/blog`, the landing page for Nord's blog.
- "Community" - links to `/community`, the landing page of the Nord
  community channels.

> Branding

To represent Nord's branding, the left-sided container consists of the
Nord logo and a caption/label. It links to Nord Docs landing page to
allow quick access to the root (`/`) page.

> Theme Mode Switcher

The user action component mentioned in the introduction paragraph above
allows to toggle between the available global theme modes. Both modes
are represented through icons where a sun is used for "bright snow
flurry" and a moon for "dark night frost" mode. They are implemented
using the awesome "React Pose" (1) project to animate the "rising up"
and "going down" sequences. The icons fly out and in" within the bounds
f the component that takes the form of a button.

Like documented in the Iconography design concept (2), the awesome
"Eva Icons" (3) project will is used where the "moon" and "sun" icons
represent the both available theme modes.

> Responsive Design

For reduced width views (responsive design) the header adjusts several
styles and composed components.

>> Slide Menu

The main navigation link list will be hidden and replaced by the user
action element (button) that toggles an animated slide down menu
containing the navigation links. The drop down will start right below
the header and takes up the available height and width to cover the
full screen. As soon as the animation starts all scroll events will be
removed from the underlying content (body) using the
"body-scroll-lock" (4) project. This prevents users from scrolling the
content below the menu when the menu itself overflows the Y-axis and
shows a scroll bar.

>> Behavior

To allow quick access while also being inconspicuous, the component is
"sticky" at the top of the site, but will collapse as soon as the user
scrolls down. It'll only switch into expanded mode when at top of the
site.
In expanded mode, the height of the header is larger and the
caption/label of the logo is visible. As soon as switching into
collapsed mode the height decreases and the caption fades out with a
smooth transition animation.

To achieve the resizing animation based on the scroll position the
"subscribe-ui-event" (5) project is used to listen to scroll events. It
provides throttling by default, only calls `document.body.scrollTop`
and `window.innerWidth` once and uses `requestAnimationFrame` for the
best performance.

References:
  (1) https://popmotion.io/pose
  (2) #74
  (3)  https://akveo.github.io/eva-icons
  (4) https://www.npmjs.com/package/body-scroll-lock
  (5) https://www.npmjs.com/package/subscribe-ui-event

Associated epic: GH-63
GH-64
  • Loading branch information
arcticicestudio committed Dec 13, 2018
1 parent 064fabe commit a47492f
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/components/layouts/core/BaseLayout/BaseLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@
import React, { Fragment } from "react";
import PropTypes from "prop-types";

import Header from "organisms/core/Header";
import Page from "containers/core/Page";
import Root from "containers/core/Root";

/**
* The base page layout providing the main container that wraps the content.
*
* @author Arctic Ice Studio <[email protected]>
* @author Sven Greb <[email protected]>
* @since 0.3.0
*/
const BaseLayout = ({ children }) => (
<Root>
<Fragment>
<Header />
<Page>{children}</Page>
</Fragment>
</Root>
Expand Down
9 changes: 9 additions & 0 deletions src/components/layouts/core/BaseLayout/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
/*
* Copyright (C) 2018-present Arctic Ice Studio <[email protected]>
* Copyright (C) 2018-present Sven Greb <[email protected]>
*
* Project: Nord Docs
* Repository: https://github.com/arcticicestudio/nord-docs
* License: MIT
*/

export { default } from "./BaseLayout";
242 changes: 242 additions & 0 deletions src/components/organisms/core/Header/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/*
* Copyright (C) 2018-present Arctic Ice Studio <[email protected]>
* Copyright (C) 2018-present Sven Greb <[email protected]>
*
* Project: Nord Docs
* Repository: https://github.com/arcticicestudio/nord-docs
* License: MIT
*/

import React, { Fragment, PureComponent } from "react";
import PropTypes from "prop-types";
import { PoseGroup } from "react-pose";
import { subscribe } from "subscribe-ui-event";
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from "body-scroll-lock";

import { A } from "atoms/core/HTMLElements";
import { Menu } from "atoms/core/vectors/icons";
import { GlobalThemeMode } from "containers/core/Root";
import { ROUTE_ROOT } from "config/routes/mappings";
import navigationItems from "data/components/organisms/core/Header/navigationItems";
import { MODE_BRIGHT_SNOW_FLURRY } from "styles/theme";

import { HEADER_HEIGHT, HEADER_HEIGHT_PINNED } from "./shared/styles";
import {
ContentBox,
Header as StyledHeader,
Logo,
LogoBannerBox,
LogoCaption,
MoonIcon,
Nav,
NavLink,
NavList,
SlideMenuBox,
SlideMenuNavLink,
SlideMenuNavList,
SlideMenuToggle,
SunIcon,
ThemeModeSwitch,
TopContentPusher,
SLIDE_MENU_NAV_LINK_CLOSED_POSE,
SLIDE_MENU_NAV_LINK_INITIAL_POSE,
SLIDE_MENU_NAV_LINK_OPEN_POSE,
THEME_MODE_SWITCH_ICON_INITIAL_POSE
} from "./styled";

/**
* Populates and renders the list of navigation links.
*
* @since 0.3.0
*/
const renderNavListItems = navigationItems.map(({ title, url }) => (
<NavLink key={`${title}-${url}`} to={url}>
{title}
</NavLink>
));

/**
* Populates and renders the list of slide menu navigation links.
*
* @since 0.3.0
*/
const renderSlideMenuNavListItems = navigationItems.map(({ title, url }) => (
<SlideMenuNavLink key={`${title}-${url}`} initialPose={SLIDE_MENU_NAV_LINK_INITIAL_POSE} to={url}>
{title}
</SlideMenuNavLink>
));

/**
* The header component that provides Nord's branding caption and logo, the main navigation and a button to toggle
* between the availabe global theme modes.
* The sticky position at the top of the site allows quick access to the navigation while also being inconspicuous, but
* will change into pinned (collapsed) mode as soon as the user scrolls down. It will switch into unpinned (expanded)
* mode again when at top of the site.
* In unpinned mode, the height of the header is larger and the brand caption of the logo will be visible. When
* switching into pinned mode the height will decrease and the logo caption fades out with a smooth transition
* animation.
*
* @author Arctic Ice Studio <[email protected]>
* @author Sven Greb <[email protected]>
* @since 0.3.0
*/
export default class Header extends PureComponent {
static propTypes = {
/**
* The height of the header in pixels.
* Will be converted to the corresponding REM value.
*/
height: PropTypes.number,

/**
* The height of the header in pixels when in pinned mode.
* Will be converted to the corresponding REM value.
*/
heightPinned: PropTypes.number,

/**
* The height from the top in pixels where the header will switch to pinned mode.
*/
pinStart: PropTypes.number
};

static defaultProps = {
height: HEADER_HEIGHT,
heightPinned: HEADER_HEIGHT_PINNED,
pinStart: 0
};

state = {
/**
* Indicates if the header is in "pinned" mode.
*/
isPinned: false,

/**
* Indicates if the slide menu is opened.
*/
isSlideMenuOpen: false
};

componentDidMount() {
this.uiSubscribers.push(subscribe("scroll", this.handleScroll, { useRAF: true, enableScrollInfo: true }));

/* Get the slide menu element to persist scrolling when opened. */
this.slideMenuElement = this.slideMenuRef.current;
}

static getDerivedStateFromProps(props, state) {
if (typeof window !== "undefined") {
/*
* The default "scroll-into-view" behavior of Firefox in combination with Gatsby's builtin scroll position
* handling causes the header to expand two times when switching from a route where the header was pinned
* (position was scrolled) to another route where the position is at the top, chaging the header to the unpinned
* state. As soon as the new route has been loaded the header gets mounted in unpinned mode, but due to the
* previously scrolled site position immediately changed to pinned mode and then directly back to unpinned mode
* again. This state changes resulting in a short, but clearly visible visual glitch.
* The behavior is currently only reproducible for Firefox. Other tested desktop browsers like Google Chrome,
* Chromium and Safari don't use the "scroll-into-view" technique and instead showing the correct header state
* immediately.
*
* Checking for the actual scroll position and current pinning mode allows to prevent the one unnecessary state
* change by mutating the state before the component gets commited. Note that this change is has also a posiive
* side effect for other tested browsers without the problem: When the new routes gets loaded the header will now
* be animated with the visual expand effect instead of just being rendered instantly.
*
*/
if (window.scrollY > props.pinStart && state.isPinned === false) {
return { isPinned: true };
}
}
return null;
}

componentWillUnmount() {
this.uiSubscribers.map(sub => sub.unsubscribe());

/* Ensure to release all scroll-locked elements when swithcing routes. */
clearAllBodyScrollLocks();
}

slideMenuElement = null;

slideMenuRef = React.createRef();

uiSubscribers = [];

/**
* Toggles the slide menu.
*
* @method handleSlideMenuToggle
* @return {void}
*/
handleSlideMenuToggle = () => {
const { isSlideMenuOpen } = this.state;
if (!isSlideMenuOpen) {
disableBodyScroll(this.slideMenuElement);
} else {
enableBodyScroll(this.slideMenuElement);
}
this.setState({ isSlideMenuOpen: !isSlideMenuOpen });
};

handleScroll = (event, payload) => {
const currentScrollY = payload.scroll.top;
const { pinStart } = this.props;
const { isPinned } = this.state;

if (!isPinned && currentScrollY > pinStart) {
this.setState({ isPinned: true });
} else if (currentScrollY <= pinStart) {
this.setState({ isPinned: false });
}
};

render() {
const { height, heightPinned } = this.props;
const { isSlideMenuOpen, isPinned } = this.state;

return (
<Fragment>
<TopContentPusher height={height} />
<StyledHeader height={height} heightPinned={heightPinned} isPinned={isPinned} isSlideMenuOpen={isSlideMenuOpen}>
<ContentBox centered>
<LogoBannerBox>
<A to={ROUTE_ROOT}>
<Logo size={height} />
</A>
<LogoCaption isPinned={isPinned}>Nord</LogoCaption>
</LogoBannerBox>
<Nav>
<NavList>{renderNavListItems}</NavList>
<SlideMenuToggle onClick={this.handleSlideMenuToggle}>
<Menu />
</SlideMenuToggle>
<GlobalThemeMode>
{({ toggleThemeMode, mode }) => (
<ThemeModeSwitch onClick={toggleThemeMode}>
<PoseGroup preEnterPose={THEME_MODE_SWITCH_ICON_INITIAL_POSE}>
{mode === MODE_BRIGHT_SNOW_FLURRY ? (
<MoonIcon key="moon" />
) : (
<SunIcon key="sun" reverseEnterDirection />
)}
</PoseGroup>
</ThemeModeSwitch>
)}
</GlobalThemeMode>
</Nav>
</ContentBox>
<SlideMenuBox
ref={this.slideMenuElement}
isOpen={isSlideMenuOpen}
isPinned={isPinned}
pose={isSlideMenuOpen ? SLIDE_MENU_NAV_LINK_OPEN_POSE : SLIDE_MENU_NAV_LINK_CLOSED_POSE}
>
<SlideMenuNavList>{renderSlideMenuNavListItems}</SlideMenuNavList>
</SlideMenuBox>
</StyledHeader>
</Fragment>
);
}
}
14 changes: 14 additions & 0 deletions src/components/organisms/core/Header/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright (C) 2018-present Arctic Ice Studio <[email protected]>
* Copyright (C) 2018-present Sven Greb <[email protected]>
*
* Project: Nord Docs
* Repository: https://github.com/arcticicestudio/nord-docs
* License: MIT
*/

import { HEADER_HEIGHT, HEADER_HEIGHT_PINNED } from "./shared/styles";
import Header from "./Header";

export { HEADER_HEIGHT, HEADER_HEIGHT_PINNED };
export default Header;

0 comments on commit a47492f

Please sign in to comment.