From 1a13aed1533d2990f22823985f2a86ce419cb88d Mon Sep 17 00:00:00 2001 From: olayway Date: Wed, 18 May 2022 16:26:24 +0200 Subject: [PATCH] [site/components][f]: tooltip animation --- site/components/Anchor.js | 121 ++++++--------------------------- site/components/Tooltip.js | 133 ++++++++++++++++++++++++++++++++----- site/package-lock.json | 129 +++++++++++++++++++++++++++++++++++ site/package.json | 1 + site/styles/global.css | 4 +- 5 files changed, 268 insertions(+), 120 deletions(-) diff --git a/site/components/Anchor.js b/site/components/Anchor.js index 59137aa..0faf460 100644 --- a/site/components/Anchor.js +++ b/site/components/Anchor.js @@ -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 - - - { showTooltip && previewLoaded && - - { preview } - - } - - ; + if (absoluteContentPath(props.href)) { + return ( + ( + + )} + /> + ) } return ; }; diff --git a/site/components/Tooltip.js b/site/components/Tooltip.js index 188e131..7493a82 100644 --- a/site/components/Tooltip.js +++ b/site/components/Tooltip.js @@ -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 ( -
-
-
- { children } -
-
-
-
+ + { props.render(tooltipTriggerProps) } + + + { showTooltip && tooltipContentLoaded && + +
+
+ { tooltipContent } +
+
+
+
+
+ } +
+
+
) -}) +} diff --git a/site/package-lock.json b/site/package-lock.json index f359ab9..cece734 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -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", diff --git a/site/package.json b/site/package.json index b618c5f..529a257 100644 --- a/site/package.json +++ b/site/package.json @@ -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", diff --git a/site/styles/global.css b/site/styles/global.css index b310d67..f124617 100644 --- a/site/styles/global.css +++ b/site/styles/global.css @@ -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%); }