[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 { 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 documentExtract from '../utils/documentExtract'
import { Tooltip } from './Tooltip'
// TODO cancel request on mouseleave when it hasn't been fulfilled yet
export const Anchor = (props) => {
const { href } = props;
const router = useRouter();
const arrowRef = useRef(null);
const [ showTooltip, setShowTooltip ] = useState(false);
const [ preview, setPreview ] = useState("");
const [ previewLoaded, setPreviewLoaded ] = useState(false);
const {
x,
y,
reference,
floating,
placement,
strategy,
context,
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();
const absoluteContentPath = (href) => {
// return content path only if it points to a local file (href path is relative)
if (
href &&
href.indexOf("http:") !== 0 &&
href.indexOf("https:") !== 0
) {
const currentPageContentPath = [siteConfig.rawContentBaseUrl, router.asPath].join("");
const hrefContentPath = new URL(href, currentPageContentPath).href;
// excluding notes and claims
if (!hrefContentPath.includes("notes") && !hrefContentPath.includes("claims")) {
return hrefContentPath;
}
}
}, [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 (
href &&
href.indexOf("http:") !== 0 &&
href.indexOf("https:") !== 0 &&
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>;
if (absoluteContentPath(props.href)) {
return (
<Tooltip {...props} absolutePath={absoluteContentPath(props.href)} render={ tooltipTriggerProps => (
<a {...tooltipTriggerProps} />
)}
/>
)
}
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) => ({
@ -11,7 +28,7 @@ const tooltipBoxStyle = (theme) => ({
boxShadow: 'rgba(0, 0, 0, 0.55) 0px 0px 16px -3px',
})
const tooltipBodyStyle = () => ({
const tooltipBodyStyle = (theme) => ({
maxHeight: '3.6rem',
position: 'relative',
lineHeight: '1.2rem',
@ -31,8 +48,51 @@ const tooltipArrowStyle = ({ theme, x, y, side }) => ({
transform: "rotate(45deg)"
})
export const Tooltip = React.forwardRef((props, ref) => {
const { theme, children, arrowRef, arrowX, arrowY, placement, ...tooltipProps } = props;
export const Tooltip = (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 = {
top: 'bottom',
@ -41,19 +101,56 @@ export const Tooltip = React.forwardRef((props, ref) => {
left: 'right',
}[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 (
<div className="tooltip" {...tooltipProps} ref={ref}>
<div className="tooltip-box" style={ tooltipBoxStyle(theme) }>
<div className="tooltip-body" style={ tooltipBodyStyle() }>
{ children }
</div>
</div>
<div className="tooltip-arrow" ref={arrowRef} style={ tooltipArrowStyle({
theme,
x: arrowX,
y: arrowY,
side: arrowPlacement
}) }></div>
</div>
<Fragment>
{ props.render(tooltipTriggerProps) }
<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>
)
})
}

129
site/package-lock.json generated
View File

@ -14,6 +14,7 @@
"@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",
@ -308,6 +309,21 @@
"resolved": "https://registry.npmjs.org/@effect-ts/system/-/system-0.55.1.tgz",
"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": {
"version": "0.1.4",
"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"
}
},
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -2701,6 +2744,11 @@
"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": {
"version": "2.0.1",
"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"
}
},
"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": {
"version": "8.4.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz",
@ -5571,6 +5630,15 @@
"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": {
"version": "5.0.0",
"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",
"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": {
"version": "0.1.4",
"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",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -7914,6 +8018,11 @@
"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": {
"version": "2.0.1",
"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",
"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": {
"version": "8.4.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz",
@ -9840,6 +9960,15 @@
"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": {
"version": "5.0.0",
"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",
"@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",

View File

@ -35,7 +35,7 @@ body {
position: absolute;
bottom: 0;
right: 0;
width: 10%;
width: 30%;
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%);
}