Merge pull request #162 from life-itself/151-hover-over-previews
[site/mdx]: display short definitions of key words on hover
This commit is contained in:
commit
7def64b5c2
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Tooltip } from './Tooltip';
|
||||||
|
import siteConfig from '../config/siteConfig.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for adding previews on hovering over anchor tags with relative paths
|
||||||
|
*/
|
||||||
|
export const Anchor = (props) => {
|
||||||
|
/* Check if the path is relative */
|
||||||
|
const pathIsRelative = (path) => {
|
||||||
|
return path &&
|
||||||
|
path.indexOf("http:") !== 0 &&
|
||||||
|
path.indexOf("https:") !== 0 &&
|
||||||
|
path.indexOf("#") !== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathIsRelative(props.href)) {
|
||||||
|
return (
|
||||||
|
<Tooltip {...props} render={ tooltipTriggerProps => (
|
||||||
|
<a {...tooltipTriggerProps} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <a {...props} />;
|
||||||
|
};
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import ReactPlayer from 'react-player/lazy'
|
import ReactPlayer from 'react-player/lazy'
|
||||||
import { Paragraph } from './Link'
|
|
||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
import siteConfig from "../config/siteConfig"
|
import siteConfig from "../config/siteConfig"
|
||||||
|
import { Paragraph } from './Paragraph'
|
||||||
|
import { Anchor } from './Anchor'
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
Head,
|
Head,
|
||||||
p: Paragraph
|
p: Paragraph,
|
||||||
|
a: Anchor
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MdxPage({ children, editUrl }) {
|
export default function MdxPage({ children, editUrl }) {
|
||||||
|
|
@ -128,7 +130,7 @@ export default function MdxPage({ children, editUrl }) {
|
||||||
<a className="flex no-underline font-semibold text-yellow-li" href={editUrl} target="_blank">
|
<a className="flex no-underline font-semibold text-yellow-li" href={editUrl} target="_blank">
|
||||||
Edit this page
|
Edit this page
|
||||||
<span className="mx-1">
|
<span className="mx-1">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import Link from "next/link";
|
|
||||||
import ReactPlayer from "react-player";
|
import ReactPlayer from "react-player";
|
||||||
|
|
||||||
const videoLinks = [
|
const videoLinks = [
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
import { useState, useEffect, useRef, Fragment } from 'react'
|
||||||
|
import {
|
||||||
|
arrow,
|
||||||
|
autoPlacement,
|
||||||
|
FloatingPortal,
|
||||||
|
inline,
|
||||||
|
offset,
|
||||||
|
shift,
|
||||||
|
useDismiss,
|
||||||
|
useFloating,
|
||||||
|
useHover,
|
||||||
|
useFocus,
|
||||||
|
useInteractions,
|
||||||
|
useRole,
|
||||||
|
} from '@floating-ui/react-dom-interactions'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { allOtherPages } from 'contentlayer/generated';
|
||||||
|
|
||||||
|
import documentExtract from '../utils/documentExtract'
|
||||||
|
|
||||||
|
|
||||||
|
const tooltipBoxStyle = (theme) => ({
|
||||||
|
height: 'auto',
|
||||||
|
maxWidth: '40rem',
|
||||||
|
padding: '1rem',
|
||||||
|
background: theme === 'light' ? '#fff' : '#000',
|
||||||
|
color: theme === 'light' ? 'rgb(99, 98, 98)' : '#A8A8A8',
|
||||||
|
borderRadius: '4px',
|
||||||
|
boxShadow: 'rgba(0, 0, 0, 0.55) 0px 0px 16px -3px',
|
||||||
|
})
|
||||||
|
|
||||||
|
const tooltipBodyStyle = (theme) => ({
|
||||||
|
maxHeight: '4.8rem',
|
||||||
|
position: 'relative',
|
||||||
|
lineHeight: '1.2rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
})
|
||||||
|
|
||||||
|
const tooltipArrowStyle = ({ theme, x, y, side }) => ({
|
||||||
|
position: "absolute",
|
||||||
|
left: x != null ? `${x}px` : '',
|
||||||
|
top: y != null ? `${y}px` : '',
|
||||||
|
right: '',
|
||||||
|
bottom: '',
|
||||||
|
[side]: '-4px',
|
||||||
|
height: "8px",
|
||||||
|
width: "8px",
|
||||||
|
background: theme === 'light' ? '#fff' : '#000',
|
||||||
|
transform: "rotate(45deg)"
|
||||||
|
})
|
||||||
|
|
||||||
|
// export const Tooltip = ({ absolutePath, render, ...props }) => {
|
||||||
|
export const Tooltip = ({ render, ...props }) => {
|
||||||
|
const theme = 'light'; // temporarily hard-coded; light theme tbd in next PR
|
||||||
|
|
||||||
|
const arrowRef = useRef(null);
|
||||||
|
const [ showTooltip, setShowTooltip ] = useState(false);
|
||||||
|
const [ tooltipContent, setTooltipContent ] = useState("");
|
||||||
|
const [ tooltipContentLoaded, setTooltipContentLoaded ] = useState(false);
|
||||||
|
// floating-ui hook
|
||||||
|
const {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
reference, // trigger element back ref
|
||||||
|
floating, // tooltip back ref
|
||||||
|
placement, // default: 'bottom'
|
||||||
|
strategy, // default: 'absolute'
|
||||||
|
context,
|
||||||
|
middlewareData: { arrow: { x: arrowX, y: arrowY } = {}} // data for arrow positioning
|
||||||
|
} = useFloating({
|
||||||
|
open: showTooltip, // state value binding
|
||||||
|
onOpenChange: setShowTooltip, // state value setter
|
||||||
|
middleware: [
|
||||||
|
offset(5), // offset from container border
|
||||||
|
autoPlacement({ padding: 5 }), // auto place vertically
|
||||||
|
shift({ padding: 5 }), // flip horizontally if necessary
|
||||||
|
arrow({ element: arrowRef, padding: 4 }), // add arrow element
|
||||||
|
inline(), // correct position for multiline anchor tags
|
||||||
|
]
|
||||||
|
});
|
||||||
|
// floating-ui hook
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||||
|
useHover(context, { delay: 100 }),
|
||||||
|
useFocus(context),
|
||||||
|
useRole(context, { role: 'tooltip' }),
|
||||||
|
useDismiss(context, { ancestorScroll: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const triggerElementProps = getReferenceProps({ ...props, ref: reference});
|
||||||
|
const tooltipProps = getFloatingProps({
|
||||||
|
ref: floating,
|
||||||
|
style: {
|
||||||
|
position: strategy,
|
||||||
|
left: x ?? '',
|
||||||
|
top: y ?? '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrowPlacement = {
|
||||||
|
top: 'bottom',
|
||||||
|
right: 'left',
|
||||||
|
bottom: 'top',
|
||||||
|
left: 'right',
|
||||||
|
}[placement.split('-')[0]];
|
||||||
|
|
||||||
|
const fetchTooltipContent = async () => {
|
||||||
|
setTooltipContentLoaded(false);
|
||||||
|
|
||||||
|
let content;
|
||||||
|
// get tooltip content
|
||||||
|
try {
|
||||||
|
// create a temporary anchor tag to convert relative href to absolute path
|
||||||
|
const tempLink = document.createElement("a");
|
||||||
|
tempLink.href = props.href;
|
||||||
|
const filePath = tempLink.pathname.slice(1) // remove slash from the beginning
|
||||||
|
// disallow tooltips for 'notes' pages for now due to their different structure
|
||||||
|
if (filePath.includes('notes')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const page = allOtherPages.find(p => p._raw.sourceFilePath === filePath)
|
||||||
|
content = documentExtract(page.body.raw);
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTooltipContent(content);
|
||||||
|
setTooltipContentLoaded(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showTooltip) {
|
||||||
|
fetchTooltipContent();
|
||||||
|
}
|
||||||
|
}, [showTooltip])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{ render(triggerElementProps) }
|
||||||
|
<FloatingPortal>
|
||||||
|
<AnimatePresence>
|
||||||
|
{ showTooltip && tooltipContentLoaded &&
|
||||||
|
<motion.div
|
||||||
|
{...tooltipProps}
|
||||||
|
initial={{ opacity: 0, scale: 0.85 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
||||||
|
>
|
||||||
|
<div className="tooltip-box" style={ tooltipBoxStyle(theme) }>
|
||||||
|
<div className="tooltip-body" style={ tooltipBodyStyle(theme) }>
|
||||||
|
{ tooltipContent }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref={arrowRef} className="tooltip-arrow" style={ tooltipArrowStyle({
|
||||||
|
theme,
|
||||||
|
x: arrowX,
|
||||||
|
y: arrowY,
|
||||||
|
side: arrowPlacement
|
||||||
|
})}>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
}
|
||||||
|
</AnimatePresence>
|
||||||
|
</FloatingPortal>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,6 +7,7 @@
|
||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom-interactions": "^0.6.0",
|
||||||
"@headlessui/react": "^1.4.1",
|
"@headlessui/react": "^1.4.1",
|
||||||
"@heroicons/react": "^1.0.4",
|
"@heroicons/react": "^1.0.4",
|
||||||
"@mdx-js/loader": "^2.0.0",
|
"@mdx-js/loader": "^2.0.0",
|
||||||
|
|
@ -15,7 +16,9 @@
|
||||||
"@silvenon/remark-smartypants": "^1.0.0",
|
"@silvenon/remark-smartypants": "^1.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.2",
|
"@tailwindcss/typography": "^0.5.2",
|
||||||
"contentlayer": "^0.1.2",
|
"contentlayer": "^0.1.2",
|
||||||
|
"framer-motion": "^6.3.3",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"hast-util-to-string": "^2.0.0",
|
||||||
"next": "^12.1.0",
|
"next": "^12.1.0",
|
||||||
"next-contentlayer": "^0.1.2",
|
"next-contentlayer": "^0.1.2",
|
||||||
"next-seo": "^4.28.1",
|
"next-seo": "^4.28.1",
|
||||||
|
|
@ -26,9 +29,11 @@
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
"rehype-slug": "^5.0.1",
|
"rehype-slug": "^5.0.1",
|
||||||
"remark-gfm": "^3.0.0",
|
"remark-gfm": "^3.0.0",
|
||||||
|
"remark-parse": "^10.0.1",
|
||||||
"remark-slug": "^7.0.0",
|
"remark-slug": "^7.0.0",
|
||||||
"remark-toc": "^8.0.0",
|
"remark-toc": "^8.0.0",
|
||||||
"remark-wiki-link-plus": "^1.0.0"
|
"remark-wiki-link-plus": "^1.0.0",
|
||||||
|
"unist-util-find": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^10.2.6",
|
"autoprefixer": "^10.2.6",
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,14 @@ body {
|
||||||
.extra-small {
|
.extra-small {
|
||||||
font-size: 0.50rem;
|
font-size: 0.50rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* tooltip fade-out clip */
|
||||||
|
.tooltip-body::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 3.6rem; /* multiple of $line-height used on the tooltip body (defined in tooltipBodyStyle) */
|
||||||
|
height: 1.2rem; /* ($top + $height)/$line-height is the number of lines we want to clip tooltip text at*/
|
||||||
|
width: 10rem;
|
||||||
|
background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 100%);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { unified } from 'unified'
|
||||||
|
import remarkParse from 'remark-parse'
|
||||||
|
import find from 'unist-util-find'
|
||||||
|
import { toString } from 'mdast-util-to-string'
|
||||||
|
|
||||||
|
|
||||||
|
// get first paragraph found in the document
|
||||||
|
const documentExtract = (md) => {
|
||||||
|
const mdast = unified().use(remarkParse).parse(md);
|
||||||
|
let paragraph = find(mdast, (node) => node.type === "paragraph");
|
||||||
|
return toString(paragraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default documentExtract;
|
||||||
Loading…
Reference in New Issue