diff --git a/src/components/layouts/core/BaseLayout/BaseLayout.jsx b/src/components/layouts/core/BaseLayout/BaseLayout.jsx index 46e753d4..3f0eb872 100644 --- a/src/components/layouts/core/BaseLayout/BaseLayout.jsx +++ b/src/components/layouts/core/BaseLayout/BaseLayout.jsx @@ -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 + * @author Sven Greb * @since 0.3.0 */ const BaseLayout = ({ children }) => ( +
{children} diff --git a/src/components/layouts/core/BaseLayout/index.js b/src/components/layouts/core/BaseLayout/index.js index cd391add..bc312598 100644 --- a/src/components/layouts/core/BaseLayout/index.js +++ b/src/components/layouts/core/BaseLayout/index.js @@ -1 +1,10 @@ +/* + * Copyright (C) 2018-present Arctic Ice Studio + * Copyright (C) 2018-present Sven Greb + * + * Project: Nord Docs + * Repository: https://github.com/arcticicestudio/nord-docs + * License: MIT + */ + export { default } from "./BaseLayout"; diff --git a/src/components/organisms/core/Header/Header.jsx b/src/components/organisms/core/Header/Header.jsx new file mode 100644 index 00000000..7b4aca13 --- /dev/null +++ b/src/components/organisms/core/Header/Header.jsx @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2018-present Arctic Ice Studio + * Copyright (C) 2018-present Sven Greb + * + * 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 }) => ( + + {title} + +)); + +/** + * Populates and renders the list of slide menu navigation links. + * + * @since 0.3.0 + */ +const renderSlideMenuNavListItems = navigationItems.map(({ title, url }) => ( + + {title} + +)); + +/** + * 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 + * @author Sven Greb + * @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 ( + + + + + + + + + Nord + + + + + {renderSlideMenuNavListItems} + + + + ); + } +} diff --git a/src/components/organisms/core/Header/index.js b/src/components/organisms/core/Header/index.js new file mode 100644 index 00000000..2d2a8bbb --- /dev/null +++ b/src/components/organisms/core/Header/index.js @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2018-present Arctic Ice Studio + * Copyright (C) 2018-present Sven Greb + * + * 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;