Skip to content

Commit be1f9a6

Browse files
authored
Merge pull request #48 from jxom/update-sidebar-highlight-on-scroll
Tweak sidebar to update navigation highlight on scroll
2 parents 8a1c36e + 39def4b commit be1f9a6

File tree

7 files changed

+133
-19
lines changed

7 files changed

+133
-19
lines changed

Diff for: content/tutorial/nav.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
- title: Tutorial
22
items:
3-
- id: tutorial
3+
- id: before-we-start
44
title: Before We Start
55
href: /tutorial/tutorial.html#before-we-start
66
forceInternal: true

Diff for: src/components/MarkdownPage/MarkdownPage.js

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const MarkdownPage = ({
2626
authors,
2727
createLink,
2828
date,
29+
enableScrollSync,
2930
ogDescription,
3031
location,
3132
markdownRemark,
@@ -98,6 +99,7 @@ const MarkdownPage = ({
9899

99100
<div css={sharedStyles.articleLayout.sidebar}>
100101
<StickyResponsiveSidebar
102+
enableScrollSync={enableScrollSync}
101103
createLink={createLink}
102104
defaultActiveSection={findSectionForPath(
103105
location.pathname,
@@ -132,6 +134,7 @@ MarkdownPage.propTypes = {
132134
authors: PropTypes.array.isRequired,
133135
createLink: PropTypes.func.isRequired,
134136
date: PropTypes.string,
137+
enableScrollSync: PropTypes.bool,
135138
location: PropTypes.object.isRequired,
136139
markdownRemark: PropTypes.object.isRequired,
137140
sectionList: PropTypes.array.isRequired,
+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the CC-BY-4.0 license found
5+
* in the LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
import React, {Component} from 'react';
13+
import Section from './Section';
14+
15+
class ScrollSyncSection extends Component {
16+
constructor(props, context) {
17+
super(props, context);
18+
19+
this.state = {
20+
activeItemId: '',
21+
itemTopOffsets: [],
22+
};
23+
24+
this.calculateItemTopOffsets = this.calculateItemTopOffsets.bind(this);
25+
this.handleResize = this.handleResize.bind(this);
26+
this.handleScroll = this.handleScroll.bind(this);
27+
}
28+
29+
componentDidMount() {
30+
this.calculateItemTopOffsets();
31+
32+
window.addEventListener('resize', this.handleResize);
33+
window.addEventListener('scroll', this.handleScroll);
34+
}
35+
36+
componentWillUnmount() {
37+
window.removeEventListener('resize', this.handleResize);
38+
window.removeEventListener('scroll', this.handleScroll);
39+
}
40+
41+
calculateItemTopOffsets() {
42+
const {section} = this.props;
43+
44+
const itemIds = _getItemIds(section.items);
45+
this.setState({
46+
itemTopOffsets: _getElementTopOffsetsById(itemIds),
47+
});
48+
}
49+
50+
handleResize() {
51+
this.calculateItemTopOffsets();
52+
this.handleScroll();
53+
}
54+
55+
handleScroll() {
56+
const {itemTopOffsets} = this.state;
57+
const item = itemTopOffsets.find((itemTopOffset, i) => {
58+
const nextItemTopOffset = itemTopOffsets[i + 1];
59+
if (nextItemTopOffset) {
60+
return (
61+
window.scrollY >= itemTopOffset.offsetTop &&
62+
window.scrollY < nextItemTopOffset.offsetTop
63+
);
64+
}
65+
return window.scrollY >= itemTopOffset.offsetTop;
66+
});
67+
this.setState({
68+
activeItemId: item ? item.id : '',
69+
});
70+
}
71+
72+
render() {
73+
const {activeItemId} = this.state;
74+
return <Section isScrollSync activeItemId={activeItemId} {...this.props} />;
75+
}
76+
}
77+
78+
const _getItemIds = items =>
79+
items
80+
.map(item => {
81+
let subItemIds = [];
82+
if (item.subitems) {
83+
subItemIds = item.subitems.map(subitem => subitem.id);
84+
}
85+
return [item.id, ...subItemIds];
86+
})
87+
.reduce((prev, current) => prev.concat(current));
88+
89+
const _getElementTopOffsetsById = ids =>
90+
ids
91+
.map(id => {
92+
const element = document.getElementById(id);
93+
if (!element) {
94+
return null;
95+
}
96+
return {
97+
id,
98+
offsetTop: element.offsetTop,
99+
};
100+
})
101+
.filter(item => item);
102+
103+
export default ScrollSyncSection;

Diff for: src/templates/components/Sidebar/Section.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,16 @@
99

1010
'use strict';
1111

12-
import React from 'react';
1312
import {colors, media} from 'theme';
13+
import isItemActive from 'utils/isItemActive';
1414
import MetaTitle from '../MetaTitle';
1515
import ChevronSvg from '../ChevronSvg';
1616

17-
// TODO Update isActive link as document scrolls past anchor tags
18-
// Maybe used 'hashchange' along with 'scroll' to set/update active links
19-
2017
const Section = ({
18+
activeItemId,
2119
createLink,
2220
isActive,
21+
isScrollSync,
2322
location,
2423
onLinkClick,
2524
onSectionTitleClick,
@@ -67,6 +66,9 @@ const Section = ({
6766
marginTop: 5,
6867
}}>
6968
{createLink({
69+
isActive: isScrollSync
70+
? activeItemId === item.id
71+
: isItemActive(location, item),
7072
item,
7173
location,
7274
onLinkClick,
@@ -78,6 +80,9 @@ const Section = ({
7880
{item.subitems.map(subitem => (
7981
<li key={subitem.id}>
8082
{createLink({
83+
isActive: isScrollSync
84+
? activeItemId === subitem.id
85+
: isItemActive(location, subitem),
8186
item: subitem,
8287
location,
8388
onLinkClick,

Diff for: src/templates/components/Sidebar/Sidebar.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import React, {Component} from 'react';
1313
import Flex from 'components/Flex';
1414
import Section from './Section';
15+
import ScrollSyncSection from './ScrollSyncSection';
1516
import {media} from 'theme';
1617

1718
class Sidebar extends Component {
@@ -24,9 +25,17 @@ class Sidebar extends Component {
2425
}
2526

2627
render() {
27-
const {closeParentMenu, createLink, location, sectionList} = this.props;
28+
const {
29+
closeParentMenu,
30+
createLink,
31+
enableScrollSync,
32+
location,
33+
sectionList,
34+
} = this.props;
2835
const {activeSection} = this.state;
2936

37+
const SectionComponent = enableScrollSync ? ScrollSyncSection : Section;
38+
3039
return (
3140
<Flex
3241
type="nav"
@@ -46,7 +55,7 @@ class Sidebar extends Component {
4655
},
4756
}}>
4857
{sectionList.map((section, index) => (
49-
<Section
58+
<SectionComponent
5059
createLink={createLink}
5160
isActive={activeSection === section || sectionList.length === 1}
5261
key={index}

Diff for: src/templates/tutorial.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const Tutorial = ({data, location}) => {
2727

2828
return (
2929
<MarkdownPage
30+
enableScrollSync
3031
createLink={createLinkTutorial}
3132
location={location}
3233
markdownRemark={data.markdownRemark}

Diff for: src/utils/createLink.js

+5-12
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@
1212
import Link from 'gatsby-link';
1313
import React from 'react';
1414
import ExternalLinkSvg from 'templates/components/ExternalLinkSvg';
15-
import isItemActive from 'utils/isItemActive';
1615
import slugify from 'utils/slugify';
1716
import {colors, media} from 'theme';
1817

19-
const createLinkBlog = ({item, location, section}) => {
20-
const isActive = isItemActive(location, item);
21-
18+
const createLinkBlog = ({isActive, item, section}) => {
2219
return (
2320
<Link css={[linkCss, isActive && activeLinkCss]} to={item.id}>
2421
{isActive && <span css={activeLinkBefore} />}
@@ -27,7 +24,7 @@ const createLinkBlog = ({item, location, section}) => {
2724
);
2825
};
2926

30-
const createLinkCommunity = ({item, location, section}) => {
27+
const createLinkCommunity = ({isActive, item, section}) => {
3128
if (item.href) {
3229
return (
3330
<a css={[linkCss]} href={item.href} target="_blank" rel="noopener">
@@ -44,15 +41,13 @@ const createLinkCommunity = ({item, location, section}) => {
4441
);
4542
}
4643
return createLinkDocs({
44+
isActive,
4745
item,
48-
location,
4946
section,
5047
});
5148
};
5249

53-
const createLinkDocs = ({item, location, section}) => {
54-
const isActive = isItemActive(location, item);
55-
50+
const createLinkDocs = ({isActive, item, section}) => {
5651
return (
5752
<Link
5853
css={[linkCss, isActive && activeLinkCss]}
@@ -63,9 +58,7 @@ const createLinkDocs = ({item, location, section}) => {
6358
);
6459
};
6560

66-
const createLinkTutorial = ({item, location, onLinkClick, section}) => {
67-
const isActive = isItemActive(location, item);
68-
61+
const createLinkTutorial = ({isActive, item, onLinkClick, section}) => {
6962
return (
7063
<Link
7164
css={[linkCss, isActive && activeLinkCss]}

0 commit comments

Comments
 (0)