[site/components][f]: tooltip animation

This commit is contained in:
olayway 2022-05-18 16:26:24 +02:00
parent 5589b2244d
commit 1a13aed153
5 changed files with 268 additions and 120 deletions

View File

@ -1,115 +1,36 @@
import ReactDOM from 'react-dom'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useState, useEffect, useRef, Fragment } from 'react'
import {
arrow,
autoPlacement,
autoUpdate,
FloatingPortal,
offset,
shift,
useDismiss,
useFloating,
useHover,
useInteractions,
useRole,
} from '@floating-ui/react-dom-interactions'
import { Tooltip } from './Tooltip';
import siteConfig from '../config/siteConfig.js' import siteConfig from '../config/siteConfig.js'
import documentExtract from '../utils/documentExtract'
import { Tooltip } from './Tooltip'
// TODO cancel request on mouseleave when it hasn't been fulfilled yet
export const Anchor = (props) => { export const Anchor = (props) => {
const { href } = props; const { href } = props;
const router = useRouter(); const router = useRouter();
const arrowRef = useRef(null);
const [ showTooltip, setShowTooltip ] = useState(false); const absoluteContentPath = (href) => {
const [ preview, setPreview ] = useState(""); // return content path only if it points to a local file (href path is relative)
const [ previewLoaded, setPreviewLoaded ] = useState(false); if (
href &&
const { href.indexOf("http:") !== 0 &&
x, href.indexOf("https:") !== 0
y, ) {
reference, const currentPageContentPath = [siteConfig.rawContentBaseUrl, router.asPath].join("");
floating, const hrefContentPath = new URL(href, currentPageContentPath).href;
placement, // excluding notes and claims
strategy, if (!hrefContentPath.includes("notes") && !hrefContentPath.includes("claims")) {
context, return hrefContentPath;
middlewareData: { arrow: { x: arrowX, y: arrowY } = {}} }
} = useFloating({
open: showTooltip,
onOpenChange: setShowTooltip,
whileElementsMounted: autoUpdate,
middleware: [
offset(5),
autoPlacement({ padding: 5 }),
shift({ padding: 5 }),
arrow({ element: arrowRef, padding: 4 })
]
});
const { getReferenceProps, getFloatingProps } = useInteractions([
useHover(context, { delay: 100 }),
useRole(context, { role: 'tooltip' }),
useDismiss(context, { ancestorScroll: true })
]);
useEffect(() => {
if (showTooltip) {
fetchPreview();
} }
}, [showTooltip])
const fetchPreview = async () => {
setPreviewLoaded(false);
const path = new URL(props.href, document.baseURI).pathname;
const rawContentPath = [siteConfig.rawContentBaseUrl, path].join("")
const response = await fetch(rawContentPath);
if (response.status !== 200) {
// TODO
console.log(`Looks like there was a problem. Status Code: ${response.status}`)
return
}
const md = await response.text();
const extract = documentExtract(md);
setPreview(extract);
setPreviewLoaded(true);
} }
if ( if (absoluteContentPath(props.href)) {
href && return (
href.indexOf("http:") !== 0 && <Tooltip {...props} absolutePath={absoluteContentPath(props.href)} render={ tooltipTriggerProps => (
href.indexOf("https:") !== 0 && <a {...tooltipTriggerProps} />
href.includes(".md") )}
) { />
return <Fragment> )
<a {...props} {...getReferenceProps({ref: reference})} />
<FloatingPortal>
{ showTooltip && previewLoaded &&
<Tooltip
{...getFloatingProps({
ref: floating,
theme: 'light',
arrowRef,
arrowX,
arrowY,
placement,
style: {
position: strategy,
left: x ?? '',
top: y ?? '',
},
})}
>
{ preview }
</Tooltip>
}
</FloatingPortal>
</Fragment>;
} }
return <a {...props} />; return <a {...props} />;
}; };

View File

@ -1,4 +1,21 @@
import React from 'react'; 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 documentExtract from '../utils/documentExtract'
const tooltipBoxStyle = (theme) => ({ const tooltipBoxStyle = (theme) => ({
@ -11,7 +28,7 @@ const tooltipBoxStyle = (theme) => ({
boxShadow: 'rgba(0, 0, 0, 0.55) 0px 0px 16px -3px', boxShadow: 'rgba(0, 0, 0, 0.55) 0px 0px 16px -3px',
}) })
const tooltipBodyStyle = () => ({ const tooltipBodyStyle = (theme) => ({
maxHeight: '3.6rem', maxHeight: '3.6rem',
position: 'relative', position: 'relative',
lineHeight: '1.2rem', lineHeight: '1.2rem',
@ -31,8 +48,51 @@ const tooltipArrowStyle = ({ theme, x, y, side }) => ({
transform: "rotate(45deg)" transform: "rotate(45deg)"
}) })
export const Tooltip = React.forwardRef((props, ref) => { export const Tooltip = (props) => {
const { theme, children, arrowRef, arrowX, arrowY, placement, ...tooltipProps } = 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 dom 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 interactions hook
const { getReferenceProps, getFloatingProps } = useInteractions([
useHover(context, { delay: 100 }),
useFocus(context),
useRole(context, { role: 'tooltip' }),
useDismiss(context, { ancestorScroll: true }),
]);
const tooltipTriggerProps = getReferenceProps({ ...props, ref: reference});
const tooltipProps = getFloatingProps({
ref: floating,
style: {
position: strategy,
left: x ?? '',
top: y ?? '',
},
});
const arrowPlacement = { const arrowPlacement = {
top: 'bottom', top: 'bottom',
@ -41,19 +101,56 @@ export const Tooltip = React.forwardRef((props, ref) => {
left: 'right', left: 'right',
}[placement.split('-')[0]]; }[placement.split('-')[0]];
const fetchTooltipContent = async () => {
setTooltipContentLoaded(false);
const response = await fetch(props.absolutePath);
if (response.status !== 200) {
console.log(`Looks like there was a problem. Status Code: ${response.status}`)
return
}
const md = await response.text();
const extract = documentExtract(md);
setTooltipContent(extract);
setTooltipContentLoaded(true);
}
useEffect(() => {
if (showTooltip) {
fetchTooltipContent();
}
}, [showTooltip])
return ( return (
<div className="tooltip" {...tooltipProps} ref={ref}> <Fragment>
<div className="tooltip-box" style={ tooltipBoxStyle(theme) }> { props.render(tooltipTriggerProps) }
<div className="tooltip-body" style={ tooltipBodyStyle() }> <FloatingPortal>
{ children } <AnimatePresence>
</div> { showTooltip && tooltipContentLoaded &&
</div> <motion.div
<div className="tooltip-arrow" ref={arrowRef} style={ tooltipArrowStyle({ {...tooltipProps}
theme, initial={{ opacity: 0, scale: 0.85 }}
x: arrowX, animate={{ opacity: 1, scale: 1 }}
y: arrowY, exit={{ opacity: 0 }}
side: arrowPlacement transition={{ type: "spring", damping: 20, stiffness: 300 }}
}) }></div> >
</div> <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>
) )
}) }

129
site/package-lock.json generated
View File

@ -14,6 +14,7 @@
"@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", "hast-util-to-string": "^2.0.0",
"next": "^12.1.0", "next": "^12.1.0",
@ -308,6 +309,21 @@
"resolved": "https://registry.npmjs.org/@effect-ts/system/-/system-0.55.1.tgz", "resolved": "https://registry.npmjs.org/@effect-ts/system/-/system-0.55.1.tgz",
"integrity": "sha512-OEnwd9JhrV2Q5S7cke/ZgR56Hn75DSr1aIkA0PBE1edoX6GKB6nOdu8u/vPhvqjxLHfMgN8o+EVaWUHPLIC1UQ==" "integrity": "sha512-OEnwd9JhrV2Q5S7cke/ZgR56Hn75DSr1aIkA0PBE1edoX6GKB6nOdu8u/vPhvqjxLHfMgN8o+EVaWUHPLIC1UQ=="
}, },
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"optional": true,
"dependencies": {
"@emotion/memoize": "0.7.4"
}
},
"node_modules/@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
"optional": true
},
"node_modules/@esbuild-plugins/node-resolve": { "node_modules/@esbuild-plugins/node-resolve": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-resolve/-/node-resolve-0.1.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-resolve/-/node-resolve-0.1.4.tgz",
@ -2431,6 +2447,33 @@
"url": "https://www.patreon.com/infusion" "url": "https://www.patreon.com/infusion"
} }
}, },
"node_modules/framer-motion": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.3.3.tgz",
"integrity": "sha512-wo0dCnoq5vn4L8YVOPO9W54dliH78vDaX0Lj+bSPUys6Nt5QaehrS3uaYa0q5eVeikUgtTjz070UhQ94thI5Sw==",
"dependencies": {
"framesync": "6.0.1",
"hey-listen": "^1.0.8",
"popmotion": "11.0.3",
"style-value-types": "5.0.0",
"tslib": "^2.1.0"
},
"optionalDependencies": {
"@emotion/is-prop-valid": "^0.8.2"
},
"peerDependencies": {
"react": ">=16.8 || ^17.0.0 || ^18.0.0",
"react-dom": ">=16.8 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/framesync": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz",
"integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -2701,6 +2744,11 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/hey-listen": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="
},
"node_modules/html-void-elements": { "node_modules/html-void-elements": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
@ -4630,6 +4678,17 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/popmotion": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz",
"integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==",
"dependencies": {
"framesync": "6.0.1",
"hey-listen": "^1.0.8",
"style-value-types": "5.0.0",
"tslib": "^2.1.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.12", "version": "8.4.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz",
@ -5571,6 +5630,15 @@
"inline-style-parser": "0.1.1" "inline-style-parser": "0.1.1"
} }
}, },
"node_modules/style-value-types": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz",
"integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==",
"dependencies": {
"hey-listen": "^1.0.8",
"tslib": "^2.1.0"
}
},
"node_modules/styled-jsx": { "node_modules/styled-jsx": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.0.tgz", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.0.tgz",
@ -6334,6 +6402,21 @@
"resolved": "https://registry.npmjs.org/@effect-ts/system/-/system-0.55.1.tgz", "resolved": "https://registry.npmjs.org/@effect-ts/system/-/system-0.55.1.tgz",
"integrity": "sha512-OEnwd9JhrV2Q5S7cke/ZgR56Hn75DSr1aIkA0PBE1edoX6GKB6nOdu8u/vPhvqjxLHfMgN8o+EVaWUHPLIC1UQ==" "integrity": "sha512-OEnwd9JhrV2Q5S7cke/ZgR56Hn75DSr1aIkA0PBE1edoX6GKB6nOdu8u/vPhvqjxLHfMgN8o+EVaWUHPLIC1UQ=="
}, },
"@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"optional": true,
"requires": {
"@emotion/memoize": "0.7.4"
}
},
"@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
"optional": true
},
"@esbuild-plugins/node-resolve": { "@esbuild-plugins/node-resolve": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-resolve/-/node-resolve-0.1.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-resolve/-/node-resolve-0.1.4.tgz",
@ -7722,6 +7805,27 @@
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
"integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA=="
}, },
"framer-motion": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.3.3.tgz",
"integrity": "sha512-wo0dCnoq5vn4L8YVOPO9W54dliH78vDaX0Lj+bSPUys6Nt5QaehrS3uaYa0q5eVeikUgtTjz070UhQ94thI5Sw==",
"requires": {
"@emotion/is-prop-valid": "^0.8.2",
"framesync": "6.0.1",
"hey-listen": "^1.0.8",
"popmotion": "11.0.3",
"style-value-types": "5.0.0",
"tslib": "^2.1.0"
}
},
"framesync": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz",
"integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==",
"requires": {
"tslib": "^2.1.0"
}
},
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -7914,6 +8018,11 @@
"space-separated-tokens": "^2.0.0" "space-separated-tokens": "^2.0.0"
} }
}, },
"hey-listen": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="
},
"html-void-elements": { "html-void-elements": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
@ -9196,6 +9305,17 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
}, },
"popmotion": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz",
"integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==",
"requires": {
"framesync": "6.0.1",
"hey-listen": "^1.0.8",
"style-value-types": "5.0.0",
"tslib": "^2.1.0"
}
},
"postcss": { "postcss": {
"version": "8.4.12", "version": "8.4.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz",
@ -9840,6 +9960,15 @@
"inline-style-parser": "0.1.1" "inline-style-parser": "0.1.1"
} }
}, },
"style-value-types": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz",
"integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==",
"requires": {
"hey-listen": "^1.0.8",
"tslib": "^2.1.0"
}
},
"styled-jsx": { "styled-jsx": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.0.tgz", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.0.tgz",

View File

@ -16,6 +16,7 @@
"@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", "hast-util-to-string": "^2.0.0",
"next": "^12.1.0", "next": "^12.1.0",

View File

@ -35,7 +35,7 @@ body {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 0; right: 0;
width: 10%; width: 30%;
height: 1.2em; height: 1.2em;
background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 50%); background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 100%);
} }