Skip to content
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

feat(www): useActiveHash hook for highlighting links in Docs' table of contents #21762

Merged
merged 31 commits into from
Mar 20, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c4b122a
Create useActiveHash hook
jlkiri Feb 26, 2020
dceb3d5
Change hashlist to idlist
jlkiri Feb 26, 2020
8bef0e7
Remove console.log
jlkiri Feb 26, 2020
1bedc8b
Hook cleanup
jlkiri Feb 26, 2020
ca9761d
Style link based on hash
jlkiri Feb 26, 2020
e85d299
Add traversal option
jlkiri Feb 26, 2020
bd9aa2c
Move hooks to its own file
jlkiri Feb 26, 2020
7f302b1
Merge remote-tracking branch 'upstream/master' into use-active-hash
jlkiri Feb 26, 2020
7b924e4
Improve hook
jlkiri Feb 26, 2020
28b9403
Add comments, remove console.log
jlkiri Feb 26, 2020
036fcfd
Revert hash remove logic
jlkiri Feb 26, 2020
8160ad8
Remove unused colors
jlkiri Feb 26, 2020
b804b4d
Revert "Remove unused colors"
jlkiri Feb 26, 2020
b522004
Remove unused colors
jlkiri Feb 26, 2020
0a6f3aa
Refactor getHeadingIds
jlkiri Feb 26, 2020
89c4ef5
Switch to getElementById
jlkiri Feb 27, 2020
4763d30
Merge remote-tracking branch 'upstream/master' into use-active-hash
jlkiri Feb 27, 2020
1376382
Handle cases where url is absent
jlkiri Feb 28, 2020
53399bb
Do not update URL hash
jlkiri Feb 28, 2020
78632a9
Disable active link behavior on mobile
jlkiri Feb 28, 2020
e350ada
Handle mobile with media query
jlkiri Feb 28, 2020
fbd2248
Merge remote-tracking branch 'upstream/master' into use-active-hash
jlkiri Feb 28, 2020
e567fe2
Move function
Feb 29, 2020
a7dac40
Rename toc to items, add depth prop
jlkiri Mar 3, 2020
c7f8110
Merge remote-tracking branch 'upstream/master' into use-active-hash
jlkiri Mar 3, 2020
3cf10d2
Merge remote-tracking branch 'upstream/master' into use-active-hash
jlkiri Mar 10, 2020
5f051fd
Highlight subheadings
jlkiri Mar 10, 2020
63373b8
Add tests
jlkiri Mar 19, 2020
41661fd
Fix merge conflict
jlkiri Mar 19, 2020
c2ed59a
update recursion so tableOfContentsDepth is respected and something i…
gillkyle Mar 20, 2020
291850c
Merge branch 'master' into use-active-hash
gillkyle Mar 20, 2020
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
133 changes: 89 additions & 44 deletions www/src/components/docs-table-of-contents.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
/** @jsx jsx */
import { jsx } from "theme-ui"
import { Link } from "gatsby"
import { colors, mediaQueries } from "gatsby-design-tokens/dist/theme-gatsbyjs-org"
import {
mediaQueries,
breakpoints,
} from "gatsby-design-tokens/dist/theme-gatsbyjs-org"
import { useEffect, useState } from "react"
import { useActiveHash } from "../hooks/use-active-hash"

const getHeadingIds = (toc, traverseFullDepth = false) => {
const idList = []
const hashToId = str => str.slice(1)

if (toc) {
for (const item of toc) {
// Sometimes url does not exist on item. See #19851
if (item.url) {
idList.push(hashToId(item.url))
}

// Only traverse sub-items if specified (they are not displayed in ToC)
if (item.items && traverseFullDepth) {
idList.push(...getHeadingIds(item.items, true))
}
}
}

return idList
}

function isUnderDepthLimit(depth, maxDepth) {
if (maxDepth === null) {
Expand All @@ -14,54 +40,71 @@ function isUnderDepthLimit(depth, maxDepth) {

// depth and maxDepth are used to figure out how many bullets deep to render in the ToC sidebar, if no
// max depth is set via the tableOfContentsDepth field in the frontmatter, all headings will be rendered
function createItems(items, location, depth, maxDepth) {
function createItems(items, location, depth, maxDepth, activeHash, isDesktop) {
return (
items &&
items.map((item, index) => (
<li
sx={{ [mediaQueries.xl]: { fontSize: 1 } }}
key={location.pathname + (item.url || depth + `-` + index)}
>
{item.url && (
<Link
sx={{
"&&": {
color: `textMuted`,
border: 0,
transition: t =>
`all ${t.transition.speed.fast} ${t.transition.curve.default}`,
":hover": {
color: `link.color`,
borderBottom: t => `1px solid ${t.colors.link.hoverBorder}`,
items.map((item, index) => {
const isActive = isDesktop && item.url === `#${activeHash}`
return (
<li
sx={{ [mediaQueries.xl]: { fontSize: 1 } }}
key={location.pathname + (item.url || depth + `-` + index)}
>
{item.url && (
<Link
sx={{
"&&": {
color: isActive ? `link.color` : `textMuted`,
border: 0,
borderBottom: t =>
isActive
? `1px solid ${t.colors.link.hoverBorder}`
: `none`,
transition: t =>
`all ${t.transition.speed.fast} ${t.transition.curve.default}`,
":hover": {
color: `link.color`,
borderBottom: t => `1px solid ${t.colors.link.hoverBorder}`,
},
},
},
}}
getProps={({ href, location }) =>
location && location.href && location.href.includes(href)
? {
style: {
color: colors.link.color,
borderBottom: `1px solid ${colors.link.hoverBorder}`,
},
}
: null
}
to={location.pathname + item.url}
>
{item.title}
</Link>
)}
{item.items && isUnderDepthLimit(depth, maxDepth) && (
<ul sx={{ color: `textMuted`, listStyle: `none`, ml: 5 }}>
{createItems(item.items, location, depth + 1, maxDepth)}
</ul>
)}
</li>
))
}}
to={location.pathname + item.url}
>
{item.title}
</Link>
)}
{item.items && isUnderDepthLimit(depth, maxDepth) && (
<ul sx={{ color: `textMuted`, listStyle: `none`, ml: 5 }}>
{createItems(
item.items,
location,
depth + 1,
maxDepth,
activeHash,
isDesktop
)}
</ul>
)}
</li>
)
})
)
}

function TableOfContents({ page, location }) {
function TableOfContents({ page, toc, location }) {
gillkyle marked this conversation as resolved.
Show resolved Hide resolved
const [isDesktop, setIsDesktop] = useState(false)
const activeHash = useActiveHash(getHeadingIds(toc))

useEffect(() => {
const isDesktopQuery = window.matchMedia(`(min-width: ${breakpoints[4]})`) // 1200px

setIsDesktop(isDesktopQuery.matches)

const updateIsDesktop = e => setIsDesktop(e.matches)
isDesktopQuery.addListener(updateIsDesktop)
return () => isDesktopQuery.removeListener(updateIsDesktop)
}, [])

return page.tableOfContents.items ? (
<nav
sx={{
Expand Down Expand Up @@ -98,7 +141,9 @@ function TableOfContents({ page, location }) {
page.tableOfContents.items,
location,
1,
page.frontmatter.tableOfContentsDepth
page.frontmatter.tableOfContentsDepth,
activeHash,
isDesktop
)}
</ul>
</nav>
Expand Down
30 changes: 30 additions & 0 deletions www/src/hooks/use-active-hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEffect, useState } from "react"

export const useActiveHash = (itemIds, rootMargin = undefined) => {
const [activeHash, setActiveHash] = useState(``)

useEffect(() => {
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setActiveHash(entry.target.id)
}
})
},
{ rootMargin: rootMargin || `0% 0% -80% 0%` }
)

itemIds.forEach(id => {
observer.observe(document.getElementById(id))
})

return () => {
itemIds.forEach(id => {
observer.unobserve(document.getElementById(id))
})
}
}, [])

return activeHash
}
2 changes: 1 addition & 1 deletion www/src/templates/template-docs-markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function DocsTemplate({ data, location, pageContext: { next, prev } }) {
},
}}
>
<TableOfContents location={location} page={page} />
<TableOfContents toc={toc} location={location} page={page} />
</div>
)}
<div
Expand Down