diff --git a/src/components/BaseContentLayout.jsx b/src/components/BaseContentLayout.jsx
index 544ba016..463abc5d 100644
--- a/src/components/BaseContentLayout.jsx
+++ b/src/components/BaseContentLayout.jsx
@@ -9,35 +9,37 @@ import Terminav from "@components/Terminav";
export default function BaseContentLayout({ children, className }) {
return (
-
- {/* Main Content */}
-
+
+ {/* Main Content */}
+ h1:nth-of-type(1)': {
- color: 'accent',
- paddingTop: '0.4rem',
- marginBottom: '2rem',
- },
- }}>
- {children}
-
+ '& > h1:nth-of-type(1)': {
+ color: 'accent',
+ paddingTop: '0.4rem',
+ marginBottom: '2rem',
+ },
+ }}>
+ {children}
+
- {/* Footer */}
-
-
-
-
+ {/* Footer */}
+ {/* */}
+ {/* */}
+ {/* */}
+
+
);
}
diff --git a/src/components/CoreLayout.jsx b/src/components/CoreLayout.jsx
index 71941fe7..19ea77d3 100644
--- a/src/components/CoreLayout.jsx
+++ b/src/components/CoreLayout.jsx
@@ -1,11 +1,15 @@
/** @jsxImportSource theme-ui */
import SiteHelmet from '@components/SiteHelmet';
+import Terminav from "@components/Terminav";
export default function CoreLayout({ children }) {
return (
<>
+ {/* */}
+ {/* Show the Terminav when a user _tries_ to scroll */}
+
{children}
>
);
diff --git a/src/components/FullscreenNoScrollLayout.jsx b/src/components/FullscreenNoScrollLayout.jsx
index 58bf13db..8a87ea33 100644
--- a/src/components/FullscreenNoScrollLayout.jsx
+++ b/src/components/FullscreenNoScrollLayout.jsx
@@ -4,13 +4,13 @@ import { Global, Container } from "theme-ui";
import { useState } from 'react';
import CoreLayout from '@components/CoreLayout';
-import Terminav from "@components/Terminav";
export default function FullscreenNoScrollLayout({ children, className }) {
return (
;
},
- ['cd']: function navigateTo(link) {
+ ['cd']: function navigateTo(link, close) {
+ // Just reset if they try to navigate to the current page.
+ if (link == currentPageLink()) {
+ // Close the terminav.
+ close();
+ // Prep the display text for the next time they open it.
+ return COMMANDS['ls']();
+ }
+
// Navigate to the link destination.
if (link in NAV_LINKS) {
navigate(NAV_LINKS[link].props.href);
- return null;
+ return close();
}
// Bonus feature: `cd -` acts as a back-button.
if (link == '-') {
navigate(-1);
- return null;
+ return close();
}
// If you don't provide a known link, we show you your options.
@@ -64,94 +75,121 @@ const COMMANDS = {
/** A navigation component with the look and feel of a terminal. */
export default function Terminav({ scrollVisibilityThreshold = 85 }) {
+ // NOTE(dabrady) Certain interactivity is not suited to touch interfaces.
+ var interactive = !isTouchDevice();
+
var inputRef = useRef();
var [output, setOutput] = useState(COMMANDS.ls());
- var [opacity, setOpacity] = useState(0);
+
+ var [opacity, _setOpacity] = useState(0);
+ function show() {
+ _setOpacity(1);
+ // Prevent body from scrolling underneath.
+ document.body.setAttribute('style', 'overflow: hidden');
+ }
+ function hide() {
+ _setOpacity(0);
+ // Reactivate scrolling on body.
+ document.body.removeAttribute('style');
+ }
/**
- * This effect makes the Terminav fade in based on user's scroll position.
+ * This effect makes the Terminav fade in when you scroll past the edge of the screen.
*/
- useEffect(function() {
- function adjustOpacity(ev) {
- var focused = inputRef?.current && document.activeElement == inputRef.current;
- if (focused) return;
-
- var root = document.documentElement;
- var navPosition = inputRef.current.getBoundingClientRect();
- var navInView = navPosition.bottom <= root.clientHeight;
-
- var maxScrollPosition = root.scrollHeight - root.clientHeight;
- var currentScrollPosition = root.scrollTop;
- var scrollProgress = (currentScrollPosition / maxScrollPosition) * 100;
-
- if (
- maxScrollPosition == 0
- || isNaN(scrollProgress)
- || (navInView && scrollProgress >= scrollVisibilityThreshold)
- ) {
- setOpacity(1);
- // Focus on visible when not on touch devices.
- // Input focus on touch devices tends to automatically open a keyboard,
- // and that's annoying.
- if (ev.type == 'wheel' && navInView) {
- inputRef.current.focus();
- }
- } else {
- setOpacity(0);
- }
- }
-
- // NOTE(dabrady) Using 'wheel' event instead of 'scroll' here because it
- // fires when a user _attempts_ to scroll, even if the page is not scrollable.
- // The 'scroll' event only fires when the page actually scrolls.
- window.addEventListener('wheel', adjustOpacity);
- window.addEventListener('touchmove', adjustOpacity);
- return function stopListening() {
- window.removeEventListener('wheel', adjustOpacity);
- window.removeEventListener('touchmove', adjustOpacity);
- };
- }, []);
+ useEdgeScrollListener({
+ handler: function showTerminav() {
+ show();
+ // Focus once visible.
+ setTimeout(function focusTerminav() {
+ inputRef.current?.focus();
+ }, 250);
+ },
+ pauseWhen: function terminavIsVisible() {
+ return opacity == 1;
+ },
+ deps: [opacity],
+ });
return (
- `calc(${lineHeights.body} * 4rem)`,
- marginBottom: '0.6rem',
- visibility: opacity ? 'visible' : 'hidden',
- opacity,
- transition: 'visibility 0.3s linear, opacity 0.3s linear',
- }}>
- command(...args));
- inputRef.current.value = '';
+ /* Fullscreen overlay */
+
+ {/* Overlay body (for relative positioning of contents) */}
+
-
-
-
- {output}
+ {/* Overlay contents */}
+
+ `calc(${lineHeights.body} * 4rem)`,
+ marginBottom: '0.6rem',
+ }}>
+ command(...args, hide));
+ inputRef.current.value = '';
+ }}
+ >
+
+ {interactive &&
+ }
+
+ {output}
+
+
+
);
}
@@ -216,9 +254,13 @@ function MonoList({ heading, items }) {
);
}
+/** Finds the nav link to the current page. */
+function currentPageLink() {
+ var currentPage = typeof window != 'undefined' ? window.location.pathname : '';
+ return _.findKey(NAV_LINKS, ['props.href', currentPage]);
+}
+
/** Filters out the current page from the set of possible nav links. */
function availableNavLinks() {
- var currentPage = typeof window != 'undefined' ? window.location.pathname : '';
- var currentPageLink = _.findKey(NAV_LINKS, ['props.href', currentPage]);
- return _.omit(NAV_LINKS, currentPageLink);
+ return _.omit(NAV_LINKS, currentPageLink());
}
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index c051c84b..6753711e 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -18,7 +18,7 @@ export default function Home() {
{/* */}
{/* Show the Terminav when a user _tries_ to scroll */}
-
+ {/* */}
0;
+ var atTopEdge = root.scrollTop <= 0;
+ var atBottomEdge = root.scrollTop >= maxScrollPosition;
+
+ // If the user attempts to scroll passed the edge of the page, trigger the handler.
+ if (wheeledUp && atTopEdge) {
+ handler();
+ } else if(wheeledDown && atBottomEdge) {
+ handler();
+ }
+ }
+
+ // Track the wheeling. (There're no native 'wheelstart' or 'wheelstop' events.)
+ wheeling = true;
+ clearTimeout(wheelingTimer);
+ wheelingTimer = setTimeout(function recordWheelingStop() {
+ wheeling = false;
+ }, 250); // Consider wheel to have stopped if enough time passes between events.
+ }
+ }, deps);
+
+ /* For touch-based devices */
+ useEffect(function () {
+ if (pauseWhen()) return cleanup;
+
+ window.addEventListener('touchstart', recordTouchStarts);
+ window.addEventListener('touchmove', triggerHandlerAtEdge);
+
+ return cleanup;
+
+ /********/
+
+ var touchStart = 0;
+
+ function recordTouchStarts(ev) {
+ touchStart = ev.changedTouches[0].screenY;
+ }
+
+ function triggerHandlerAtEdge(ev) {
+ /**
+ * Show the nav bar when the user attempts to scroll past the top or bottom of the
+ * current page.
+ *
+ * NOTE(dabrady) After much experimentation and research, I found that on my iPhone,
+ * the `window.innerHeight` property updates in response to the mobile browser hiding
+ * or showing the toolbar, and `document.documentElement.clientHeight` assumes the
+ * toolbar is hidden.
+ **/
+ var maxScrollPosition = document.documentElement.scrollHeight - window.innerHeight;
+ var touchEnd = ev.changedTouches[0].screenY;
+
+ var swipedUp = touchEnd >= touchStart;
+ var swipedDown = touchEnd < touchStart;
+ var atTopEdge = window.scrollY <= 0;
+ var atBottomEdge = window.scrollY >= maxScrollPosition;
+ if (swipedUp && atTopEdge) {
+ handler();
+ } else if (swipedDown && atBottomEdge) {
+ handler();
+ }
+ }
+
+ function cleanup() {
+ window.removeEventListener('touchmove', triggerHandlerAtEdge);
+ window.removeEventListener('touchstart', recordTouchStarts);
+ }
+ }, deps);
+}
diff --git a/src/utils/isTouchDevice.js b/src/utils/isTouchDevice.js
new file mode 100644
index 00000000..9456587a
--- /dev/null
+++ b/src/utils/isTouchDevice.js
@@ -0,0 +1,5 @@
+/** A simple but effective way of detecting touch-based devices. **/
+export default function isTouchDevice() {
+ return (typeof window != 'undefined' && 'ontouchstart' in window)
+ || (typeof navigator != 'undefined' && navigator.maxTouchPoints > 0);
+}