Skip to content

Commit

Permalink
feat(toc): 포스트 상세 화면에서 포스트 목차 노출 (#17)
Browse files Browse the repository at this point in the history
- 포스트 상세 화면에서 화면 우측에 포스트 목차를
  `sticky` position 형태로 노출하도록 함
- 포스트 목차를 폭이 넓은 스크린에서만 보여주도록 함
- 폭이 넓은 스크린에서 여백 공간을 활용하고자
  lg, xl에 대한 브레이크포인트를 변경하고 그에 따라
  테이블 목차(toc)의 margin을 다르게 설정함

Signed-off-by: chayeoi <chayeoikeem@gmail.com>
  • Loading branch information
chayeoi committed Feb 26, 2020
1 parent 1658822 commit a16bfc9
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 6 deletions.
58 changes: 58 additions & 0 deletions src/components/sticky.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/** @jsx jsx */
import { jsx } from '@emotion/core'
import _ from 'lodash/fp'
import { useCallback, useEffect, useRef } from 'react'

interface Props {
top?: number;
}

const Sticky: React.FC<Props> = ({ top = 0, ...otherProps }) => {
const elementRef = useRef<HTMLDivElement>(null)
const y = useRef(0)

const handleScroll = useCallback(_.throttle(50, () => {
const element = elementRef.current

if (!element) {
return
}

const fixed = element.style.position === 'fixed'
const nextFixed = window.pageYOffset + top > y.current

if (fixed !== nextFixed) {
element.style.position = nextFixed ? 'fixed' : 'static'
element.style.top = nextFixed ? `${top}px` : 'auto'
}
}), [top])

useEffect(() => {
const element = elementRef.current

if (!element) {
return
}

const rect = element.getBoundingClientRect()

y.current = rect.top + window.pageYOffset
}, [])

useEffect(() => {
window.addEventListener('scroll', handleScroll)

return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [handleScroll])

return (
<div
ref={elementRef}
{...otherProps}
/>
)
}

export default Sticky
75 changes: 75 additions & 0 deletions src/components/table-of-contents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/** @jsx jsx */
import { css, jsx, SerializedStyles } from '@emotion/core'
import _ from 'lodash/fp'

import { TOC_ITEM_SPACING } from '../constants'
import { Theme } from '../models/Theme'
import TocItem from '../models/TocItem'

interface Props {
toc: TocItem[];
}

const TableOfContents: React.FC<Props> = ({ toc, ...otherProps }) => {
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}`}>
{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}`}>
{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}`}>
{item.title}
</a>
</li>
), item.items)}
</ul>
)}
</li>
), item.items)}
</ul>
)}
</li>
), toc)}
</ul>
</div>
)
}

const s = {
root: (theme: Theme): SerializedStyles => css`
display: none;
width: 240px;
color: ${theme.palette.text.secondary};
font-size: ${theme.typography.pxToRem(14)};
${theme.breakpoints.media.lg} {
display: block;
}
${theme.breakpoints.media.xl} {
width: 256px;
}
`,
toc: (theme: Theme): SerializedStyles => css`
padding-left: 32px;
a:hover, a:focus {
color: ${theme.palette.primary.main};
}
`,
item: css`
position: relative;
`,
}

export default TableOfContents
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export * from './link'
export * from './media-types'
export * from './palette'
export * from './size'
export * from './spacing'
export * from './storage'
1 change: 1 addition & 0 deletions src/constants/spacing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const TOC_ITEM_SPACING = 20
4 changes: 2 additions & 2 deletions src/styles/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ const common = {
xs: 0,
sm: 600,
md: 960,
lg: 1280,
xl: 1960,
lg: 1334,
xl: 1680,
},
get media() {
return _.reduce((acc, label) => ({
Expand Down
31 changes: 27 additions & 4 deletions src/templates/blog-post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@ import React, { useMemo } from 'react'
import Layout from '../components/layout'
import Profile from '../components/profile'
import SEO from '../components/seo'
import Sticky from '../components/sticky'
import TableOfContents from '../components/table-of-contents'
import Utterances from '../components/utterances'
import { CONTAINER_MAX_WIDTH } from '../constants'
import { Theme } from '../models/Theme'
import UnstructuredTocItem from '../models/UnstructuredTocItem'
import { getToc } from '../utils'

interface Props {
data: {
mdx: {
body: string;
tableOfContents: {
items: UnstructuredTocItem[];
};
frontmatter: {
title: string;
description: string;
Expand All @@ -37,7 +45,6 @@ interface Props {
};
};
};
body: string;
};
site: {
siteMetadata: {
Expand Down Expand Up @@ -72,6 +79,8 @@ const BlogPost: React.FC<Props> = ({ data, location }) => {
? `${data.site.siteMetadata.siteUrl}${publicURL}`
: ''

const toc = useMemo(() => getToc(data.mdx.tableOfContents.items, 3), [data.mdx.tableOfContents.items])

const meta = useMemo(() => _.filter(item => Boolean(item.content), [
{
property: 'article:author',
Expand Down Expand Up @@ -132,6 +141,11 @@ const BlogPost: React.FC<Props> = ({ data, location }) => {
</li>
)}
</ul>
<div css={s.toc}>
<Sticky top={112}>
<TableOfContents toc={toc} />
</Sticky>
</div>
</div>
{hasCover
? (
Expand Down Expand Up @@ -161,12 +175,11 @@ const s = {
`,
header: (theme: Theme): SerializedStyles => css`
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-end;
height: 360px;
color: #fff;
color: #ffffff;
::before {
content: '';
position: absolute;
Expand All @@ -179,6 +192,7 @@ const s = {
}
`,
container: css`
position: relative;
width: 100%;
padding: 24px 16px;
max-width: ${CONTAINER_MAX_WIDTH}px;
Expand Down Expand Up @@ -221,6 +235,15 @@ const s = {
font-weight: 300;
text-align: right;
`,
toc: (theme: Theme): SerializedStyles => css`
position: absolute;
left: 100%;
top: calc(100% + 96px);
margin-left: 1.5rem;
${theme.breakpoints.media.xl} {
margin-left: 4.5rem;
}
`,
wrapper: css`
max-width: ${CONTAINER_MAX_WIDTH}px;
margin: 0 auto;
Expand All @@ -231,8 +254,8 @@ const s = {
export const query = graphql`
query BlogPostQuery($id: String) {
mdx(id: { eq: $id }) {
id
body
tableOfContents
frontmatter {
title
description
Expand Down

0 comments on commit a16bfc9

Please sign in to comment.