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

feat(v2): add back to top button #4912

Merged
merged 11 commits into from
Jul 28, 2021
122 changes: 122 additions & 0 deletions packages/docusaurus-theme-classic/src/theme/BackToTopButton/index.tsx
Original file line number Diff line number Diff line change
@@ -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<CancelScrollTop | null>(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 (
<button
className={clsx('clean-btn', styles.backToTopButton, {
[styles.backToTopButtonShow]: show,
})}
type="button"
title="Scroll to top"
onClick={() => smoothScrollTop()}>
<svg viewBox="0 0 24 24" width="28">
<path
d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"
fill="currentColor"
/>
</svg>
</button>
);
}

export default BackToTopButton;
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 3 additions & 0 deletions packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -64,6 +65,8 @@ function DocPageContent({
tag: docVersionSearchTag(pluginId, version),
}}>
<div className={styles.docPage}>
<BackToTopButton />

{sidebar && (
<aside
className={clsx(styles.docSidebarContainer, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ const useHideableNavbar = (hideOnScroll: boolean): useHideableNavbarReturns => {
}, []);

useScrollPosition(
({scrollY: scrollTop}, {scrollY: lastScrollTop}) => {
(currentPosition, lastPosition) => {
const scrollTop = currentPosition.scrollY;
const lastScrollTop = lastPosition?.scrollY;
if (!hideOnScroll) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,32 @@ import {useEffect, useRef} from 'react';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import type {ScrollPosition} from '@theme/hooks/useScrollPosition';

const getScrollPosition = (): ScrollPosition => ({
scrollX: ExecutionEnvironment.canUseDOM ? window.pageXOffset : 0,
scrollY: ExecutionEnvironment.canUseDOM ? window.pageYOffset : 0,
});
const getScrollPosition = (): ScrollPosition | null => {
return ExecutionEnvironment.canUseDOM
? {
scrollX: window.pageXOffset,
scrollY: window.pageYOffset,
}
: null;
};

const useScrollPosition = (
effect?: (position: ScrollPosition, lastPosition: ScrollPosition) => void,
effect: (
position: ScrollPosition,
lastPosition: ScrollPosition | null,
) => void,
deps = [],
): void => {
const scrollPosition = useRef(getScrollPosition());
const lastPositionRef = useRef<ScrollPosition | null>(getScrollPosition());

const handleScroll = () => {
const currentScrollPosition = getScrollPosition();
const currentPosition = getScrollPosition()!;

if (effect) {
effect(currentScrollPosition, scrollPosition.current);
effect(currentPosition, lastPositionRef.current);
}

scrollPosition.current = currentScrollPosition;
lastPositionRef.current = currentPosition;
};

useEffect(() => {
Expand Down
5 changes: 4 additions & 1 deletion packages/docusaurus-theme-classic/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@ declare module '@theme/hooks/useScrollPosition' {
export type ScrollPosition = {scrollX: number; scrollY: number};

const useScrollPosition: (
effect?: (position: ScrollPosition, lastPosition: ScrollPosition) => void,
effect: (
position: ScrollPosition,
lastPosition: ScrollPosition | null,
) => void,
deps?: unknown[],
) => void;
export default useScrollPosition;
Expand Down