diff --git a/website/src/components/docs/MarkdownActionsDropdown/index.js b/website/src/components/docs/MarkdownActionsDropdown/index.js index 802acb3b..87f4a323 100644 --- a/website/src/components/docs/MarkdownActionsDropdown/index.js +++ b/website/src/components/docs/MarkdownActionsDropdown/index.js @@ -2,58 +2,62 @@ * Markdown actions dropdown (view/copy as .md). * Local override of docusaurus-markdown-source-plugin's dropdown so that * category index pages (e.g. /docs/foundations/) resolve to index.md instead - * of intro.md (which only exists for the root /docs/ page). + * of intro.md (which only exists for the root /docs & /docs/ page). + * When path has a trailing slash (e.g. /docs/contribution/code-style-guide/#hash), + * we try single-doc URL first (code-style-guide.md), then category index (index.md). */ -import React, { useState, useRef, useEffect } from 'react'; - -function getMarkdownUrl(currentPath) { - if (!currentPath.startsWith('/docs/')) return null; - if (currentPath.endsWith('/')) { - // Root docs index is intro; all other category indexes are index.md - return currentPath === '/docs/' - ? `${currentPath}intro.md` - : `${currentPath}index.md`; - } - return `${currentPath}.md`; -} +import React, { useState, useRef, useCallback } from 'react'; +import { useResolvedMarkdownUrl } from '../../../hooks/useResolvedMarkdownUrl'; +import { useClickOutside } from '../../../hooks/useClickOutside'; export default function MarkdownActionsDropdown() { const [copied, setCopied] = useState(false); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); - const currentPath = typeof window !== 'undefined' ? window.location.pathname : ''; - const isDocsPage = currentPath.startsWith('/docs/'); - const markdownUrl = getMarkdownUrl(currentPath); + const rawPath = typeof window !== 'undefined' ? window.location.pathname : ''; + const isDocsPage = rawPath === '/docs' || rawPath.startsWith('/docs/'); + const { candidates, urlReady, markdownUrl } = useResolvedMarkdownUrl(rawPath); + const closeDropdown = useCallback(() => setIsOpen(false), []); - useEffect(() => { - if (!isOpen) return; - const handleClickOutside = (event) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { - setIsOpen(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, [isOpen]); + useClickOutside(dropdownRef, isOpen, closeDropdown); - if (!isDocsPage || !markdownUrl) { + if (!isDocsPage || !candidates) { return null; } const handleOpenMarkdown = () => { + if (!urlReady || !markdownUrl) return; window.open(markdownUrl, '_blank'); setIsOpen(false); }; const handleCopyMarkdown = async () => { + if (!urlReady || !markdownUrl) return; try { - const response = await fetch(markdownUrl); - if (!response.ok) { - throw new Error('Failed to fetch markdown'); + const urlToFetch = markdownUrl; + const fetchMarkdown = () => + fetch(urlToFetch).then((r) => { + if (r.ok) return r.text(); + if (candidates.fallback && urlToFetch === candidates.primary) { + return fetch(candidates.fallback).then((r2) => { + if (!r2.ok) throw new Error('Failed to fetch markdown'); + return r2.text(); + }); + } + throw new Error('Failed to fetch markdown'); + }); + // iOS Safari: clipboard.write() must run in the same user gesture as the click; + // ClipboardItem + Promise keeps the gesture context after fetch(). + if (typeof ClipboardItem !== 'undefined') { + const item = new ClipboardItem({ + 'text/plain': fetchMarkdown().then((text) => new Blob([text], { type: 'text/plain' })), + }); + await navigator.clipboard.write([item]); + } else { + const text = await fetchMarkdown(); + await navigator.clipboard.writeText(text); } - const markdown = await response.text(); - await navigator.clipboard.writeText(markdown); setCopied(true); setTimeout(() => { @@ -78,8 +82,8 @@ export default function MarkdownActionsDropdown() { aria-expanded={isOpen} > Open Markdown - - + + @@ -88,10 +92,11 @@ export default function MarkdownActionsDropdown() { @@ -100,21 +105,21 @@ export default function MarkdownActionsDropdown() { {children}; + } + return href ? ( + + {children} + + ) : ( + {children} + ); +} + +function BreadcrumbsItem({children, active}) { + return ( +
  • + {children} +
  • + ); +} + +export default function DocBreadcrumbs() { + const breadcrumbs = useSidebarBreadcrumbs(); + const homePageRoute = useHomePageRoute(); + if (!breadcrumbs) { + return null; + } + return ( + <> + +
    + +
    + +
    +
    + + ); +} diff --git a/website/src/theme/DocBreadcrumbs/styles.module.css b/website/src/theme/DocBreadcrumbs/styles.module.css new file mode 100644 index 00000000..a400c5d9 --- /dev/null +++ b/website/src/theme/DocBreadcrumbs/styles.module.css @@ -0,0 +1,11 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.breadcrumbsContainer { + --ifm-breadcrumb-size-multiplier: 0.8; + margin-bottom: 0.8rem; +} diff --git a/website/src/theme/Root.js b/website/src/theme/Root.js index 67f64354..50ba9317 100644 --- a/website/src/theme/Root.js +++ b/website/src/theme/Root.js @@ -5,11 +5,7 @@ import React from 'react'; import { Toaster } from 'react-hot-toast'; import NavbarEnhancements from '@site/src/components/navigation/NavbarEnhancements'; -import useInjectMarkdownActionsDropdown from '@site/src/hooks/useInjectMarkdownActionsDropdown'; - export default function Root({children}) { - useInjectMarkdownActionsDropdown(); - return ( <>