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

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

Merged
merged 7 commits into from
Aug 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set only 1 time

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