diff --git a/packages/docusaurus-theme-classic/src/theme/BackToTopButton/index.tsx b/packages/docusaurus-theme-classic/src/theme/BackToTopButton/index.tsx new file mode 100644 index 0000000000..5ea763f0fd --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/BackToTopButton/index.tsx @@ -0,0 +1,122 @@ +/** + * 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. + */ + +import React, {useRef, useState} from 'react'; +import clsx from 'clsx'; +import useScrollPosition from '@theme/hooks/useScrollPosition'; + +import styles from './styles.module.css'; + +const threshold = 300; + +// Not all have support for smooth scrolling (particularly Safari mobile iOS) +// TODO proper detection is currently unreliable! +// see https://github.com/wessberg/scroll-behavior-polyfill/issues/16 +const SupportsNativeSmoothScrolling = false; +// const SupportsNativeSmoothScrolling = ExecutionEnvironment.canUseDOM && 'scrollBehavior' in document.documentElement.style; + +type CancelScrollTop = () => void; + +function smoothScrollTopNative(): CancelScrollTop { + window.scrollTo({top: 0, behavior: 'smooth'}); + return () => { + // Nothing to cancel, it's natively cancelled if user tries to scroll down + }; +} + +function smoothScrollTopPolyfill(): CancelScrollTop { + let raf: number | null = null; + function rafRecursion() { + const currentScroll = document.documentElement.scrollTop; + if (currentScroll > 0) { + raf = requestAnimationFrame(rafRecursion); + window.scrollTo(0, Math.floor(currentScroll * 0.85)); + } + } + rafRecursion(); + + return () => { + // Break the recursion + // Prevents the user from "fighting" against that recursion producing a weird UX + raf && cancelAnimationFrame(raf); + }; +} + +type UseSmoothScrollTopReturn = { + // We use a cancel function because the non-native smooth scroll-top implementation must be interrupted if user scroll down + smoothScrollTop: () => void; + cancelScrollToTop: CancelScrollTop; +}; + +function useSmoothScrollToTop(): UseSmoothScrollTopReturn { + const lastCancelRef = useRef(null); + + function smoothScrollTop(): void { + lastCancelRef.current = SupportsNativeSmoothScrolling + ? smoothScrollTopNative() + : smoothScrollTopPolyfill(); + } + + return { + smoothScrollTop, + cancelScrollToTop: () => lastCancelRef.current?.(), + }; +} + +function BackToTopButton(): JSX.Element { + const {smoothScrollTop, cancelScrollToTop} = useSmoothScrollToTop(); + const [show, setShow] = useState(false); + + useScrollPosition(({scrollY: scrollTop}, lastPosition) => { + // No lastPosition means component is just being mounted. + // Not really a scroll event from the user, so we ignore it + if (!lastPosition) { + return; + } + const lastScrollTop = lastPosition.scrollY; + + const isScrollingUp = scrollTop < lastScrollTop; + + if (!isScrollingUp) { + cancelScrollToTop(); + } + + if (scrollTop < threshold) { + setShow(false); + return; + } + + if (isScrollingUp) { + const documentHeight = document.documentElement.scrollHeight; + const windowHeight = window.innerHeight; + if (scrollTop + windowHeight < documentHeight) { + setShow(true); + } + } else { + setShow(false); + } + }, []); + + return ( + + ); +} + +export default BackToTopButton; diff --git a/packages/docusaurus-theme-classic/src/theme/BackToTopButton/styles.module.css b/packages/docusaurus-theme-classic/src/theme/BackToTopButton/styles.module.css new file mode 100644 index 0000000000..af86872f1f --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/BackToTopButton/styles.module.css @@ -0,0 +1,39 @@ +/** + * 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. + */ + +:root { + --docusaurus-btt-background: var(--ifm-color-primary); + --docusaurus-btt-color: #fff; +} + +.backToTopButton { + display: flex; + align-items: center; + justify-content: center; + position: fixed; + right: 1.3rem; + bottom: 1.3rem; + border-radius: 50%; + background: var(--docusaurus-btt-background); + color: var(--docusaurus-btt-color); + width: 3rem; + height: 3rem; + z-index: var(--ifm-z-index-fixed); + box-shadow: 0 0.125rem 0.3125rem 0 rgba(0, 0, 0, 0.3); + transition: all var(--ifm-transition-fast) ease-in-out; + opacity: 0; + transform: scale(0); +} + +.backToTopButton:hover { + opacity: 0.8; +} + +.backToTopButtonShow { + opacity: 1; + transform: scale(1); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx index 28b35177a1..6a73a492ef 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx @@ -18,6 +18,7 @@ import NotFound from '@theme/NotFound'; import type {DocumentRoute} from '@theme/DocItem'; import type {Props} from '@theme/DocPage'; import IconArrow from '@theme/IconArrow'; +import BackToTopButton from '@theme/BackToTopButton'; import {matchPath} from '@docusaurus/router'; import {translate} from '@docusaurus/Translate'; @@ -64,6 +65,8 @@ function DocPageContent({ tag: docVersionSearchTag(pluginId, version), }}>
+ + {sidebar && (