Skip to content

Commit

Permalink
feat: 포스트 상세 화면에서 목차에 대한 scrollspy 구현 (#17)
Browse files Browse the repository at this point in the history
`IntersectionObserver` API를 사용하여
스크롤 정도에 따라 현재 화면에 나타난 섹션의 제목을
highlight해주는 scrollspy를 구현함

Signed-off-by: chayeoi <chayeoikeem@gmail.com>
  • Loading branch information
chayeoi committed Feb 26, 2020
1 parent a4ab6dc commit 8e8c21c
Showing 1 changed file with 76 additions and 4 deletions.
80 changes: 76 additions & 4 deletions src/components/table-of-contents.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** @jsx jsx */
import { css, jsx, SerializedStyles } from '@emotion/core'
import { useTheme } from 'emotion-theming'
import _ from 'lodash/fp'
import { useCallback, useEffect, useState } from 'react'

import { TOC_ITEM_SPACING } from '../constants'
import { Theme } from '../models/Theme'
Expand All @@ -11,26 +13,92 @@ interface Props {
}

const TableOfContents: React.FC<Props> = ({ toc, ...otherProps }) => {
const [activeSlug, setActiveSlug] = useState<string | null>(null)

const theme: Theme = useTheme()

const observe = useCallback((data: TocItem[], observer: IntersectionObserver): void => {
_.forEach((item: TocItem): void => {
const element = document.querySelector<HTMLHeadingElement>(`#${item?.slug as string}`)

if (!element) {
return
}

observer.observe(element)

if (item.items) {
observe(item.items, observer)
}
}, data)
}, [])

useEffect(() => {
const options = {
rootMargin: '0px 0px -80% 0px',
}
const observer = new IntersectionObserver(entries => {
_.forEach(entry => {
if (entry.isIntersecting) {
setActiveSlug(entry.target.id)
}
}, entries)
}, options)

observe(toc, observer)

return () => {
observer.disconnect()
}
}, [observe, toc])

return (
<div css={s.root} {...otherProps}>
<ul css={s.toc}>
{_.map(item => (
<li key={item.slug} css={s.item}>
<a href={`#${item.slug as string}`}>
<a
css={s.anchor}
style={activeSlug === item.slug
? {
color: theme.palette.primary.main,
transform: 'scale(1.05) translateX(8px)',
}
: undefined}
href={`#${item.slug as string}`}
>
{item.title}
</a>
{item.items && (
<ul style={{ marginLeft: TOC_ITEM_SPACING }}>
{_.map(item => (
<li key={item.slug} css={s.item}>
<a href={`#${item.slug as string}`}>
<a
css={s.anchor}
style={activeSlug === item.slug
? {
color: theme.palette.primary.main,
transform: 'scale(1.05) translateX(8px)',
}
: undefined}
href={`#${item.slug as string}`}
>
{item.title}
</a>
{item.items && (
<ul style={{ marginLeft: TOC_ITEM_SPACING }}>
{_.map(item => (
<li key={item.slug} css={s.item}>
<a href={`#${item.slug as string}`}>
<a
css={s.anchor}
style={activeSlug === item.slug
? {
color: theme.palette.primary.main,
transform: 'scale(1.05) translateX(8px)',
}
: undefined}
href={`#${item.slug as string}`}
>
{item.title}
</a>
</li>
Expand All @@ -51,7 +119,7 @@ const TableOfContents: React.FC<Props> = ({ toc, ...otherProps }) => {
const s = {
root: (theme: Theme): SerializedStyles => css`
display: none;
width: 240px;
width: 224px;
color: ${theme.palette.text.secondary};
font-size: ${theme.typography.pxToRem(14)};
${theme.breakpoints.media.lg} {
Expand All @@ -70,6 +138,10 @@ const s = {
item: css`
position: relative;
`,
anchor: css`
display: block;
transition: color 0.2s, transform 0.2s;
`,
}

export default TableOfContents

0 comments on commit 8e8c21c

Please sign in to comment.