-
-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement
Header
core organism and base layout component
> 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
1 parent
064fabe
commit a47492f
Showing
4 changed files
with
269 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |