Skip to content

Tweak sidebar to update navigation highlight on scroll #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion content/tutorial/nav.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
- title: Tutorial
items:
- id: tutorial
- id: before-we-start
title: Before We Start
href: /tutorial/tutorial.html#before-we-start
forceInternal: true
Expand Down
3 changes: 3 additions & 0 deletions src/components/MarkdownPage/MarkdownPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const MarkdownPage = ({
authors,
createLink,
date,
enableScrollSync,
ogDescription,
location,
markdownRemark,
Expand Down Expand Up @@ -98,6 +99,7 @@ const MarkdownPage = ({

<div css={sharedStyles.articleLayout.sidebar}>
<StickyResponsiveSidebar
enableScrollSync={enableScrollSync}
createLink={createLink}
defaultActiveSection={findSectionForPath(
location.pathname,
Expand Down Expand Up @@ -132,6 +134,7 @@ MarkdownPage.propTypes = {
authors: PropTypes.array.isRequired,
createLink: PropTypes.func.isRequired,
date: PropTypes.string,
enableScrollSync: PropTypes.bool,
location: PropTypes.object.isRequired,
markdownRemark: PropTypes.object.isRequired,
sectionList: PropTypes.array.isRequired,
Expand Down
103 changes: 103 additions & 0 deletions src/templates/components/Sidebar/ScrollSyncSection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the CC-BY-4.0 license found
* in the LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

'use strict';

import React, {Component} from 'react';
import Section from './Section';

class ScrollSyncSection extends Component {
constructor(props, context) {
super(props, context);

this.state = {
activeItemId: '',
itemTopOffsets: [],
};

this.calculateItemTopOffsets = this.calculateItemTopOffsets.bind(this);
this.handleResize = this.handleResize.bind(this);
this.handleScroll = this.handleScroll.bind(this);
}

componentDidMount() {
this.calculateItemTopOffsets();

window.addEventListener('resize', this.handleResize);
window.addEventListener('scroll', this.handleScroll);
}

componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
window.removeEventListener('scroll', this.handleScroll);
}

calculateItemTopOffsets() {
const {section} = this.props;

const itemIds = _getItemIds(section.items);
this.setState({
itemTopOffsets: _getElementTopOffsetsById(itemIds),
});
}

handleResize() {
this.calculateItemTopOffsets();
this.handleScroll();
}

handleScroll() {
const {itemTopOffsets} = this.state;
const item = itemTopOffsets.find((itemTopOffset, i) => {
const nextItemTopOffset = itemTopOffsets[i + 1];
if (nextItemTopOffset) {
return (
window.scrollY >= itemTopOffset.offsetTop &&
window.scrollY < nextItemTopOffset.offsetTop
);
}
return window.scrollY >= itemTopOffset.offsetTop;
});
this.setState({
activeItemId: item ? item.id : '',
});
}

render() {
const {activeItemId} = this.state;
return <Section isScrollSync activeItemId={activeItemId} {...this.props} />;
}
}

const _getItemIds = items =>
items
.map(item => {
let subItemIds = [];
if (item.subitems) {
subItemIds = item.subitems.map(subitem => subitem.id);
}
return [item.id, ...subItemIds];
})
.reduce((prev, current) => prev.concat(current));

const _getElementTopOffsetsById = ids =>
ids
.map(id => {
const element = document.getElementById(id);
if (!element) {
return null;
}
return {
id,
offsetTop: element.offsetTop,
};
})
.filter(item => item);

export default ScrollSyncSection;
13 changes: 9 additions & 4 deletions src/templates/components/Sidebar/Section.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@

'use strict';

import React from 'react';
import {colors, media} from 'theme';
import isItemActive from 'utils/isItemActive';
import MetaTitle from '../MetaTitle';
import ChevronSvg from '../ChevronSvg';

// TODO Update isActive link as document scrolls past anchor tags
// Maybe used 'hashchange' along with 'scroll' to set/update active links

const Section = ({
activeItemId,
createLink,
isActive,
isScrollSync,
location,
onLinkClick,
onSectionTitleClick,
Expand Down Expand Up @@ -67,6 +66,9 @@ const Section = ({
marginTop: 5,
}}>
{createLink({
isActive: isScrollSync
? activeItemId === item.id
: isItemActive(location, item),
item,
location,
onLinkClick,
Expand All @@ -78,6 +80,9 @@ const Section = ({
{item.subitems.map(subitem => (
<li key={subitem.id}>
{createLink({
isActive: isScrollSync
? activeItemId === subitem.id
: isItemActive(location, subitem),
item: subitem,
location,
onLinkClick,
Expand Down
13 changes: 11 additions & 2 deletions src/templates/components/Sidebar/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import React, {Component} from 'react';
import Flex from 'components/Flex';
import Section from './Section';
import ScrollSyncSection from './ScrollSyncSection';
import {media} from 'theme';

class Sidebar extends Component {
Expand All @@ -24,9 +25,17 @@ class Sidebar extends Component {
}

render() {
const {closeParentMenu, createLink, location, sectionList} = this.props;
const {
closeParentMenu,
createLink,
enableScrollSync,
location,
sectionList,
} = this.props;
const {activeSection} = this.state;

const SectionComponent = enableScrollSync ? ScrollSyncSection : Section;

return (
<Flex
type="nav"
Expand All @@ -46,7 +55,7 @@ class Sidebar extends Component {
},
}}>
{sectionList.map((section, index) => (
<Section
<SectionComponent
createLink={createLink}
isActive={activeSection === section || sectionList.length === 1}
key={index}
Expand Down
1 change: 1 addition & 0 deletions src/templates/tutorial.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const Tutorial = ({data, location}) => {

return (
<MarkdownPage
enableScrollSync
createLink={createLinkTutorial}
location={location}
markdownRemark={data.markdownRemark}
Expand Down
17 changes: 5 additions & 12 deletions src/utils/createLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,10 @@
import Link from 'gatsby-link';
import React from 'react';
import ExternalLinkSvg from 'templates/components/ExternalLinkSvg';
import isItemActive from 'utils/isItemActive';
import slugify from 'utils/slugify';
import {colors, media} from 'theme';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isItemActive import can be removed now that it's no longer being used.


const createLinkBlog = ({item, location, section}) => {
const isActive = isItemActive(location, item);

const createLinkBlog = ({isActive, item, section}) => {
return (
<Link css={[linkCss, isActive && activeLinkCss]} to={item.id}>
{isActive && <span css={activeLinkBefore} />}
Expand All @@ -29,7 +26,7 @@ const createLinkBlog = ({item, location, section}) => {
);
};

const createLinkCommunity = ({item, location, section}) => {
const createLinkCommunity = ({isActive, item, section}) => {
if (item.href) {
return (
<a css={[linkCss]} href={item.href} target="_blank" rel="noopener">
Expand All @@ -46,15 +43,13 @@ const createLinkCommunity = ({item, location, section}) => {
);
}
return createLinkDocs({
isActive,
item,
location,
section,
});
};

const createLinkDocs = ({item, location, section}) => {
const isActive = isItemActive(location, item);

const createLinkDocs = ({isActive, item, section}) => {
return (
<Link
css={[linkCss, isActive && activeLinkCss]}
Expand All @@ -65,9 +60,7 @@ const createLinkDocs = ({item, location, section}) => {
);
};

const createLinkTutorial = ({item, location, onLinkClick, section}) => {
const isActive = isItemActive(location, item);

const createLinkTutorial = ({isActive, item, onLinkClick, section}) => {
return (
<Link
css={[linkCss, isActive && activeLinkCss]}
Expand Down