Skip to content
Merged
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
80 changes: 27 additions & 53 deletions site/src/components/landing/ScrollBackHome.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,28 @@
import {useEffect} from 'react';
import {useEffect, useRef} from 'react';
import {useHistory} from '@docusaurus/router';
import useBaseUrl from '@docusaurus/useBaseUrl';
import {useRoutePath} from '@site/src/config/navigation';

/** The page the landing hero hands off to, and the page it hands back from. */
const API_REFERENCE = '/docs/hiecm/v3/api';

/**
* A new gesture is one that starts after a pause. Momentum from a flick keeps
* delivering `wheel` events at frame rate, so a gap this long never appears
* inside one gesture, and always appears before the next one.
*/
const NEW_GESTURE_MS = 350;

/** How far a finger has to travel down, measured from the top, to leave. */
const TOUCH_TRAVEL = 70;

/**
* The other half of the landing page's scroll handoff: at the very top of the
* API references, scrolling up returns to the landing page.
*
* `wheel` and `touchmove` here, not `scroll`, and this is the one place that
* is right. At scrollY 0 the page cannot scroll up any further, so no
* `scroll` event ever fires; the gesture has to be read before the browser
* discards it.
* discards it. The intent threshold is on the wheel delta rather than on
* position for the same reason.
*
* Only from the reference page, and only from its very top, so an ordinary
* scroll back up through a long page ends at the top and stops there. A
* second, deliberate upward gesture is what leaves. What separates the two is
* the pause between them: an upward flick that runs to the top keeps sending
* momentum events after it arrives, and those events are indistinguishable
* from a deliberate push except by their timing.
* second, deliberate upward gesture is what leaves.
*/
export default function ScrollBackHome(): null {
const history = useHistory();
const pathname = useRoutePath();
// The landing page sits at the site root, which is the base URL and not
// necessarily '/'. `history.push` is given a browser path and applies no
// base of its own, so the base has to be on the path already.
const home = useBaseUrl('/');
const armed = useRef(false);

useEffect(() => {
const onReference =
Expand All @@ -54,58 +38,48 @@ export default function ScrollBackHome(): null {
document.documentElement.dataset.leavingBack = 'true';
window.setTimeout(() => {
delete document.documentElement.dataset.leavingBack;
history.push(home);
history.push('/');
}, 220);
};

let lastWheel = 0;
const onWheel = (event: WheelEvent) => {
const now = event.timeStamp;
const sinceLast = now - lastWheel;
lastWheel = now;

if (!atTop()) return;
if (event.deltaY > 0) return;
// Still the flick that brought the page up here, or its momentum. The
// reader asked to reach the top, not to go past it.
if (sinceLast < NEW_GESTURE_MS) return;
if (event.deltaY < -8) go();
};

// Measured from where the finger was when the page reached the top, not
// from where the touch began: a long swipe up through the page would
// otherwise arrive at the top with the full distance already travelled.
let travelFrom: number | null = null;
const onTouchStart = () => {
travelFrom = null;
};
const onTouchMove = (event: TouchEvent) => {
const y = event.touches[0]?.clientY ?? 0;
if (!atTop()) {
travelFrom = null;
armed.current = false;
return;
}
if (travelFrom === null) {
travelFrom = y;
// Arm on reaching the top, fire on the next upward push. Without the
// arming step, momentum left over from scrolling up through the page
// carries straight past the top and leaves without being asked to.
if (event.deltaY > 0) {
armed.current = false;
return;
}
if (y - travelFrom > TOUCH_TRAVEL) go();
if (!armed.current) {
armed.current = true;
return;
}
if (event.deltaY < -8) go();
};
const onTouchEnd = () => {
travelFrom = null;

let touchStart = 0;
const onTouchStart = (event: TouchEvent) => {
touchStart = event.touches[0]?.clientY ?? 0;
};
const onTouchMove = (event: TouchEvent) => {
if (!atTop()) return;
const dy = (event.touches[0]?.clientY ?? 0) - touchStart;
if (dy > 70) go();
};

window.addEventListener('wheel', onWheel, {passive: true});
window.addEventListener('touchstart', onTouchStart, {passive: true});
window.addEventListener('touchmove', onTouchMove, {passive: true});
window.addEventListener('touchend', onTouchEnd, {passive: true});
return () => {
window.removeEventListener('wheel', onWheel);
window.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('touchend', onTouchEnd);
};
}, [history, home, pathname]);
}, [history, pathname]);

return null;
}
Loading