From afaa26a3a5b6201e696e4816bddb534895160213 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sat, 27 Aug 2022 13:27:45 +0200 Subject: [PATCH] refactor toc, fix toc's styles on rtl, use `ref.current` instead `document.getElementsByClassName` (#717) * refactor toc, fix toc's styles on rtl, use `ref.current` instead `document.getElementsByClassName` * group both `mask-image` * fix --- .changeset/sweet-islands-compare.md | 5 + .../nextra-theme-docs/src/components/toc.tsx | 177 +++++++++--------- .../src/contexts/active-anchor.tsx | 2 +- .../nextra-theme-docs/src/contexts/index.ts | 1 - packages/nextra-theme-docs/src/index.tsx | 6 +- .../nextra-theme-docs/src/mdx-components.tsx | 13 +- packages/nextra-theme-docs/src/styles.css | 57 ++---- 7 files changed, 116 insertions(+), 145 deletions(-) create mode 100644 .changeset/sweet-islands-compare.md diff --git a/.changeset/sweet-islands-compare.md b/.changeset/sweet-islands-compare.md new file mode 100644 index 00000000000..64796acdcbb --- /dev/null +++ b/.changeset/sweet-islands-compare.md @@ -0,0 +1,5 @@ +--- +'nextra-theme-docs': patch +--- + +refactor toc, fix toc's styles on rtl, use `ref.current` instead `document.getElementsByClassName` diff --git a/packages/nextra-theme-docs/src/components/toc.tsx b/packages/nextra-theme-docs/src/components/toc.tsx index 94c60b27414..d2f4d2aff3a 100644 --- a/packages/nextra-theme-docs/src/components/toc.tsx +++ b/packages/nextra-theme-docs/src/components/toc.tsx @@ -1,4 +1,4 @@ -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' @@ -6,7 +6,7 @@ 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 => { @@ -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(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 ( -
  • - - {text} - -
  • - ) -} - 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(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 ( -
    -
    +
    +
    {hasHeadings && ( -
      + <>

      On This Page

      - {headings.map(heading => { - const text = getHeadingText(heading) - const slug = slugger.slug(text) - - return ( - - ) - })} -
    +
      + {items.map(({ slug, text, depth }) => ( +
    • + + {text} + +
    • + ))} +
    + )} {hasMetaInfo ? (
    {config.feedbackLink ? ( @@ -159,11 +158,9 @@ export function TOC({ ) : null} - {config.tocExtraContent ? ( -
    - {renderComponent(config.tocExtraContent)} -
    - ) : null} + {config.tocExtraContent + ? renderComponent(config.tocExtraContent) + : null}
    ) : null}
    diff --git a/packages/nextra-theme-docs/src/contexts/active-anchor.tsx b/packages/nextra-theme-docs/src/contexts/active-anchor.tsx index 5bee9e73cd4..df7c8b5bec2 100644 --- a/packages/nextra-theme-docs/src/contexts/active-anchor.tsx +++ b/packages/nextra-theme-docs/src/contexts/active-anchor.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react' -export type ActiveAnchor = Record< +type ActiveAnchor = Record< string, { isActive?: boolean diff --git a/packages/nextra-theme-docs/src/contexts/index.ts b/packages/nextra-theme-docs/src/contexts/index.ts index 08b1918bcdf..2355b9a4c58 100644 --- a/packages/nextra-theme-docs/src/contexts/index.ts +++ b/packages/nextra-theme-docs/src/contexts/index.ts @@ -1,5 +1,4 @@ export { - ActiveAnchor, useActiveAnchor, useSetActiveAnchor, ActiveAnchorProvider diff --git a/packages/nextra-theme-docs/src/index.tsx b/packages/nextra-theme-docs/src/index.tsx index e6dcd2c3f35..85cb112a937 100644 --- a/packages/nextra-theme-docs/src/index.tsx +++ b/packages/nextra-theme-docs/src/index.tsx @@ -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 @@ -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 : ( -
    +
    ) ) : ( ) diff --git a/packages/nextra-theme-docs/src/mdx-components.tsx b/packages/nextra-theme-docs/src/mdx-components.tsx index abf87ce977c..8e657e712ed 100644 --- a/packages/nextra-theme-docs/src/mdx-components.tsx +++ b/packages/nextra-theme-docs/src/mdx-components.tsx @@ -83,18 +83,19 @@ const createHeaderLink = ( id, ...props }: ComponentProps<'h2'>): ReactElement { - setActiveAnchor = useSetActiveAnchor() + setActiveAnchor ??= useSetActiveAnchor() const obRef = useRef(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!] @@ -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', diff --git a/packages/nextra-theme-docs/src/styles.css b/packages/nextra-theme-docs/src/styles.css index bd52c2c8eb8..c74e35b0db4 100644 --- a/packages/nextra-theme-docs/src/styles.css +++ b/packages/nextra-theme-docs/src/styles.css @@ -42,14 +42,6 @@ body { box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.1) inset; } } - @media (prefers-contrast: more) { - .nextra-nav-container-blur { - box-shadow: 0 0 0 1px black; - .dark & { - box-shadow: 0 0 0 1px white; - } - } - } @supports ( (-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px)) ) { @@ -107,15 +99,6 @@ body { } } - .nextra-sidebar-container { - mask-image: linear-gradient(to bottom, transparent, #000 20px), - linear-gradient(to left, #000 10px, transparent 10px); - - &.with-menu.nextra-scrollbar::-webkit-scrollbar-track { - margin-bottom: 76px; - } - } - /* Sidebar */ .nextra-sidebar { -webkit-touch-callout: none; @@ -281,16 +264,6 @@ body { } @media (prefers-contrast: more) { - .nextra-toc-meta { - box-shadow: none; - border-top: 1px solid #999 !important; - a { - @apply text-gray-800 dark:!text-gray-50; - } - } - article h2 { - border-color: #999 !important; - } .nextra-nav-container nav .nextra-nav-link { @apply text-gray-700 dark:text-gray-100; &.active { @@ -309,10 +282,10 @@ body { .nextra-navigation-links { border-color: #999 !important; } - .nextra-toc ul li a { - @apply text-gray-900 underline dark:text-gray-50; - &[aria-selected='true'] { - @apply text-primary-500; + .nextra-nav-container-blur { + box-shadow: 0 0 0 1px black; + .dark & { + box-shadow: 0 0 0 1px white; } } } @@ -361,23 +334,15 @@ article { } } -.nextra-toc { - .nextra-toc-content { - mask-image: linear-gradient(to bottom, transparent, #000 20px), - linear-gradient(to left, #000 10px, transparent 10px); - } - ul { - @apply m-0 list-none break-words; - hyphens: auto; - &:first-child { - @apply mt-0; - } - } - li { - @apply my-2; - } +.nextra-toc, .nextra-sidebar-container { + mask-image: linear-gradient(to bottom, transparent, #000 20px), + linear-gradient(to left, #000 10px, transparent 10px); } +.nextra-sidebar-container.with-menu.nextra-scrollbar::-webkit-scrollbar-track { + margin-bottom: 76px; + } + /* Search */ .nextra-search ul { max-height: min(calc(100vh - 5rem - env(safe-area-inset-bottom)), 400px);