Skip to content

Commit

Permalink
refactor toc, fix toc's styles on rtl, use ref.current instead `doc…
Browse files Browse the repository at this point in the history
…ument.getElementsByClassName` (#717)

* refactor toc, fix toc's styles on rtl, use `ref.current` instead `document.getElementsByClassName`

* group both `mask-image`

* fix
  • Loading branch information
Dimitri POSTOLOV authored Aug 27, 2022
1 parent 37b4445 commit afaa26a
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 145 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-islands-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'nextra-theme-docs': patch
---

refactor toc, fix toc's styles on rtl, use `ref.current` instead `document.getElementsByClassName`
177 changes: 87 additions & 90 deletions packages/nextra-theme-docs/src/components/toc.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { ReactElement, useRef, useEffect } from 'react'
import React, { ReactElement, useEffect, useRef, useMemo } from 'react'
import cn from 'clsx'
import Slugger from 'github-slugger'
import { Heading } from 'nextra'
import parseGitUrl from 'parse-git-url'
import scrollIntoView from 'scroll-into-view-if-needed'

import { renderComponent, getHeadingText, getGitIssueUrl } from '../utils'
import { useConfig, ActiveAnchor, useActiveAnchor } from '../contexts'
import { useConfig, useActiveAnchor } from '../contexts'
import { Anchor } from './anchor'

const getEditUrl = (filepath?: string): string => {
Expand All @@ -28,116 +28,115 @@ const getEditUrl = (filepath?: string): string => {
return '#'
}

function Item({
heading,
slug,
activeAnchor
}: {
heading: Heading
slug: string
activeAnchor: ActiveAnchor
}): ReactElement {
const text = getHeadingText(heading)
const state = activeAnchor[slug]
const ref = useRef<HTMLLIElement>(null)

useEffect(() => {
const el = ref.current
const [toc] = document.getElementsByClassName('nextra-toc')
if (state?.isActive && el && toc) {
scrollIntoView(el, {
behavior: 'smooth',
block: 'center',
inline: 'center',
scrollMode: 'always',
boundary: toc
})
}
}, [state?.isActive])

return (
<li
className={cn(
'scroll-my-6 scroll-py-6',
{
1: '',
2: '',
3: 'ml-4',
4: 'ml-8',
5: 'ml-12',
6: 'ml-16'
}[heading.depth]
)}
ref={ref}
>
<a
href={`#${slug}`}
className={cn(
'inline-block',
heading.depth === 2 && 'font-semibold',
state?.isActive
? 'text-primary-500 subpixel-antialiased'
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
)}
aria-selected={state?.isActive}
>
{text}
</a>
</li>
)
}

export function TOC({
headings,
filepathWithName
filepathWithName,
className
}: {
headings: Heading[]
filepathWithName: string
className: string
}): ReactElement {
const slugger = new Slugger()
const activeAnchor = useActiveAnchor()
const config = useConfig()
const tocRef = useRef<HTMLDivElement>(null)

headings = headings.filter(h => h.type === 'heading' && h.depth > 1)
const items = useMemo<
{ text: string; slug: string; depth: 2 | 3 | 4 | 5 | 6 }[]
>(
() =>
headings
.filter(heading => heading.type === 'heading' && heading.depth > 1)
.map(heading => {
const text = getHeadingText(heading)
return {
text,
slug: slugger.slug(text),
depth: heading.depth as any
}
}),
[headings]
)

const hasHeadings = headings.length > 0
const hasHeadings = items.length > 0
const hasMetaInfo =
config.feedbackLink || config.footerEditLink || config.tocExtraContent

const activeSlug = Object.entries(activeAnchor).find(
([, { isActive }]) => isActive
)?.[0]

useEffect(() => {
if (!activeSlug) return
const anchor = tocRef.current?.querySelector(`li > a[href="#${activeSlug}"`)

if (anchor) {
scrollIntoView(anchor, {
behavior: 'smooth',
block: 'center',
inline: 'center',
scrollMode: 'always',
boundary: tocRef.current
})
}
}, [activeSlug])

const linkClassName = cn(
'text-xs font-medium text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100',
'contrast-more:text-gray-800 contrast-more:dark:text-gray-50'
)

return (
<div className="nextra-toc order-last hidden w-64 flex-shrink-0 px-4 text-sm xl:block">
<div className="nextra-toc-content sticky top-16 -mr-4 max-h-[calc(100vh-4rem-env(safe-area-inset-bottom))] overflow-y-auto pr-4 pt-8">
<div ref={tocRef} className={cn('mx-4', className)}>
<div
className={cn(
'sticky top-16 overflow-y-auto pr-4 pt-8 text-sm [hyphens:auto]',
'ltr:-mr-4 rtl:-ml-4 max-h-[calc(100vh-4rem-env(safe-area-inset-bottom))]'
)}
>
{hasHeadings && (
<ul>
<>
<p className="mb-4 font-semibold tracking-tight">On This Page</p>
{headings.map(heading => {
const text = getHeadingText(heading)
const slug = slugger.slug(text)

return (
<Item
heading={heading}
activeAnchor={activeAnchor}
slug={slug}
key={slug}
/>
)
})}
</ul>
<ul>
{items.map(({ slug, text, depth }) => (
<li className="my-2 scroll-my-6 scroll-py-6" key={slug}>
<a
href={`#${slug}`}
className={cn(
{
2: 'font-semibold',
3: 'ltr:ml-4 rtl:mr-4',
4: 'ltr:ml-8 rtl:mr-8',
5: 'ltr:ml-12 rtl:mr-12',
6: 'ltr:ml-16 rtl:mr-16'
}[depth],
activeAnchor[slug]?.isActive
? 'text-primary-500 subpixel-antialiased contrast-more:!text-primary-500'
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300',
'contrast-more:text-gray-900 contrast-more:underline contrast-more:dark:text-gray-50'
)}
>
{text}
</a>
</li>
))}
</ul>
</>
)}

{hasMetaInfo ? (
<div
className={cn(
'nextra-toc-meta',
hasHeadings &&
'mt-8 border-t bg-white pt-8 shadow-[0_-12px_16px_white] dark:bg-dark dark:shadow-[0_-12px_16px_#111]',
'sticky bottom-0 pb-8 dark:border-neutral-800'
'sticky bottom-0 pb-8 dark:border-neutral-800 flex flex-col gap-2',
'contrast-more:shadow-none contrast-more:border-t contrast-more:border-neutral-400 contrast-more:dark:border-neutral-400'
)}
>
{config.feedbackLink ? (
<Anchor
className="mb-2 block text-xs font-medium text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
className={linkClassName}
href={getGitIssueUrl({
repository: config.docsRepositoryBase,
title: `Feedback for “${config.title}”`,
Expand All @@ -151,19 +150,17 @@ export function TOC({

{config.footerEditLink ? (
<Anchor
className="mb-2 block text-xs font-medium text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
className={linkClassName}
href={getEditUrl(filepathWithName)}
newWindow
>
{renderComponent(config.footerEditLink)}
</Anchor>
) : null}

{config.tocExtraContent ? (
<div className="pt-4 leading-4">
{renderComponent(config.tocExtraContent)}
</div>
) : null}
{config.tocExtraContent
? renderComponent(config.tocExtraContent)
: null}
</div>
) : null}
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/nextra-theme-docs/src/contexts/active-anchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React, {
useState
} from 'react'

export type ActiveAnchor = Record<
type ActiveAnchor = Record<
string,
{
isActive?: boolean
Expand Down
1 change: 0 additions & 1 deletion packages/nextra-theme-docs/src/contexts/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export {
ActiveAnchor,
useActiveAnchor,
useSetActiveAnchor,
ActiveAnchorProvider
Expand Down
6 changes: 5 additions & 1 deletion packages/nextra-theme-docs/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ const InnerLayout = ({
? localeConfig.direction === 'rtl'
: config.direction === 'rtl'
const direction = isRTL ? 'rtl' : 'ltr'

useEffect(() => {
if (typeof document === 'undefined') return
// needs for `ltr:/rtl:` modifiers inside `styles.css` file
Expand All @@ -185,17 +186,20 @@ const InnerLayout = ({
const hideSidebar = !themeContext.sidebar || themeContext.layout === 'raw'
const asPopover = activeType === 'page' || hideSidebar

const tocClassName = 'nextra-toc order-last hidden w-64 flex-shrink-0 xl:block'

const tocEl =
activeType === 'page' ||
!themeContext.toc ||
themeContext.layout !== 'default' ? (
themeContext.layout === 'full' || themeContext.layout === 'raw' ? null : (
<div className="nextra-toc order-last hidden w-64 flex-shrink-0 px-4 text-sm xl:block" />
<div className={tocClassName} />
)
) : (
<TOC
headings={config.floatTOC ? headings : []}
filepathWithName={filepath + filename}
className={tocClassName}
/>
)

Expand Down
13 changes: 7 additions & 6 deletions packages/nextra-theme-docs/src/mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,19 @@ const createHeaderLink = (
id,
...props
}: ComponentProps<'h2'>): ReactElement {
setActiveAnchor = useSetActiveAnchor()
setActiveAnchor ??= useSetActiveAnchor()
const obRef = useRef<HTMLSpanElement>(null)

useEffect(() => {
if (!obRef.current) return
const heading = obRef.current
if (!heading) return

slugs.set(obRef.current, [id, (context.index += 1)])
if (obRef.current) observer.observe(obRef.current)
slugs.set(heading, [id, (context.index += 1)])
observer.observe(heading)

return () => {
observer.disconnect()
slugs.delete(obRef.current!)
slugs.delete(heading)
setActiveAnchor(f => {
const ret = { ...f }
delete ret[id!]
Expand All @@ -108,7 +109,7 @@ const createHeaderLink = (
className={cn(
'font-semibold tracking-tight',
{
h2: 'mt-10 text-3xl border-b pb-1 dark:border-primary-100/10',
h2: 'mt-10 text-3xl border-b pb-1 dark:border-primary-100/10 contrast-more:border-neutral-400 contrast-more:dark:border-neutral-400',
h3: 'mt-8 text-2xl',
h4: 'mt-8 text-xl',
h5: 'mt-8 text-lg',
Expand Down
Loading

0 comments on commit afaa26a

Please sign in to comment.