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 ReactPlayer from 'react-player/lazy'
|
||||
import { Paragraph } from './Link'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import siteConfig from "../config/siteConfig"
|
||||
import { Paragraph } from './Paragraph'
|
||||
import { Anchor } from './Anchor'
|
||||
|
||||
const components = {
|
||||
Head,
|
||||
p: Paragraph
|
||||
p: Paragraph,
|
||||
a: Anchor
|
||||
}
|
||||
|
||||
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">
|
||||
Edit this page
|
||||
<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" />
|
||||
</svg>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import Link from "next/link";
|
||||
import ReactPlayer from "react-player";
|
||||
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom-interactions": "^0.6.0",
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@heroicons/react": "^1.0.4",
|
||||
"@mdx-js/loader": "^2.0.0",
|
||||
|
|
@ -15,7 +16,9 @@
|
|||
"@silvenon/remark-smartypants": "^1.0.0",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"contentlayer": "^0.1.2",
|
||||
"framer-motion": "^6.3.3",
|
||||
"gray-matter": "^4.0.3",
|
||||
"hast-util-to-string": "^2.0.0",
|
||||
"next": "^12.1.0",
|
||||
"next-contentlayer": "^0.1.2",
|
||||
"next-seo": "^4.28.1",
|
||||
|
|
@ -26,9 +29,11 @@
|
|||
"rehype-autolink-headings": "^6.1.1",
|
||||
"rehype-slug": "^5.0.1",
|
||||
"remark-gfm": "^3.0.0",
|
||||
"remark-parse": "^10.0.1",
|
||||
"remark-slug": "^7.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": {
|
||||
"autoprefixer": "^10.2.6",
|
||||
|
|
|
|||
|
|
@ -27,3 +27,14 @@ body {
|
|||
.extra-small {
|
||||
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