[site/components][f]: floatingui tooltip + arrow

This commit is contained in:
olayway 2022-05-18 09:36:34 +02:00
parent 0b65f7d404
commit 5290e24d95
4 changed files with 124 additions and 63 deletions

View File

@ -1,13 +1,19 @@
import ReactDOM from 'react-dom'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useState, useEffect, Fragment } from 'react' import { useState, useEffect, useRef, Fragment } from 'react'
import { useFloating, useHover, useInteractions } from '@floating-ui/react-dom-interactions' import {
import { unified } from 'unified' useFloating,
import rehypeParse from 'rehype-parse' useHover,
import find from 'unist-util-find' useInteractions,
import { toString } from 'hast-util-to-string' arrow,
autoPlacement,
autoUpdate,
offset
} from '@floating-ui/react-dom-interactions'
import { Tooltip } from './Tooltip'
import getAbsolutePath from '../utils/absolutePath' import getAbsolutePath from '../utils/absolutePath'
import documentExtract from '../utils/documentExtract'
import { Tooltip } from './Tooltip'
// TODO cancel request on mouseleave when it hasn't been fulfilled yet // TODO cancel request on mouseleave when it hasn't been fulfilled yet
@ -15,30 +21,47 @@ import getAbsolutePath from '../utils/absolutePath'
export const Anchor = (props) => { export const Anchor = (props) => {
const { href } = props; const { href } = props;
const router = useRouter(); const router = useRouter();
const [ open, setOpen ] = useState(false); const arrowRef = useRef(null);
const [ loaded, setLoaded ] = useState(false);
const [ showTooltip, setShowTooltip ] = useState(false);
const [ preview, setPreview ] = useState(""); const [ preview, setPreview ] = useState("");
const { x, y, reference, floating, strategy, context } = useFloating({ const [ previewLoaded, setPreviewLoaded ] = useState(false);
open,
onOpenChange: setOpen 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(),
arrow({ element: arrowRef, padding: 4 })
]
}); });
const { getReferenceProps, getFloatingProps } = useInteractions([ const { getReferenceProps, getFloatingProps } = useInteractions([
useHover(context, props) useHover(context, props)
]); ]);
useEffect(() => { useEffect(() => {
if (open) { if (showTooltip) {
fetchPreview(); fetchPreview();
} }
}, [open]) }, [showTooltip])
const fetchPreview = async () => { const fetchPreview = async () => {
setLoaded(false); setPreviewLoaded(false);
const basePath = "http://localhost:3000"; // TODO const basePath = "http://localhost:3000"; // TODO
const currentPath = router.asPath; const currentPath = router.asPath;
const relativePath = props.href.split(".")[0]; // TBD temp remove .md const relativePath = props.href.split(".")[0]; // TBD temp remove .md
const absolutePath = getAbsolutePath({ currentPath, basePath, relativePath }); const absolutePath = getAbsolutePath({ currentPath, basePath, relativePath });
console.log(`Fetching: ${absolutePath}`);
const response = await fetch(absolutePath); const response = await fetch(absolutePath);
if (response.status !== 200) { if (response.status !== 200) {
@ -47,20 +70,10 @@ export const Anchor = (props) => {
return return
} }
const html = await response.text(); const html = await response.text();
const hast = unified().use(rehypeParse).parse(html); const extract = documentExtract(html);
console.log(hast)
const article = find(hast, (node) => {
return node.tagName === "article"
})
const main = find(article, (node) => {
return node.tagName === "main"
})
const p = find(main, (node) => {
return node.tagName === "p"
})
setPreview(toString(p)); setPreview(extract);
setLoaded(true); setPreviewLoaded(true);
} }
if ( if (
@ -71,21 +84,29 @@ export const Anchor = (props) => {
) { ) {
return <Fragment> return <Fragment>
<a {...props} {...getReferenceProps({ref: reference})} /> <a {...props} {...getReferenceProps({ref: reference})} />
{open && loaded && ( {( // TODO temp client only
// TODO temp span typeof window !== 'undefined' && window.document &&
<span ReactDOM.createPortal(
{...getFloatingProps({ (<Tooltip
ref: floating, {...getFloatingProps({
className: "tooltip", ref: floating,
style: { theme: 'light',
position: strategy, arrowRef,
left: x ?? '', arrowX,
top: y ?? '', arrowY,
}, placement,
})} style: {
> position: strategy,
{ preview } visibility: showTooltip && previewLoaded ? 'visible' : 'hidden',
</span> left: x ?? '',
top: y ?? '',
},
})}
>
{ preview }
</Tooltip>
), document.body
)
)} )}
</Fragment>; </Fragment>;
} }

View File

@ -1,28 +1,53 @@
const tooltipTextStyles = (theme) => ({ import React from 'react';
const tooltipStyles = (theme) => ({
height: 'auto',
// maxWidth: '30rem',
padding: '16px 22px', padding: '16px 22px',
fontSize: '11px',
background: theme === 'light' ? '#fff' : '#000', background: theme === 'light' ? '#fff' : '#000',
// color: theme === 'light' ? 'black' : 'white', color: theme === 'light' ? 'rgb(99, 98, 98)' : '#A8A8A8',
pointerEvents: 'none',
borderRadius: '4px', borderRadius: '4px',
position: 'absolute' boxShadow: 'rgba(0, 0, 0, 0.55) 0px 0px 16px -3px',
fontSize: '90%'
}) })
// const tooltipBoxStyles = (theme) => ({ const tooltipArrowStyles = ({ theme, x, y, side }) => ({
// color: theme === 'light' ? 'rgb(99, 98, 98)' : '#A8A8A8', position: "absolute",
// transition: "0.1s", left: x != null ? `${x}px` : '',
// width: "50vw" top: y != null ? `${y}px` : '',
// }) right: '',
bottom: '',
[side]: '-4px',
height: "8px",
width: "8px",
background: theme === 'light' ? '#fff' : '#000',
transform: "rotate(45deg)"
})
export const Tooltip = React.forwardRef((props, ref) => {
const { theme, children, arrowRef, arrowX, arrowY, placement, ...tooltipProps } = props;
console.log({ arrowRef, arrowX, arrowY, placement });
const arrowPlacement = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[placement.split('-')[0]];
export const Tooltip = (props) => {
const { theme, content } = props;
return ( return (
<div> <div {...tooltipProps} ref={ref}>
<div style={ tooltipTextStyles(theme) }> <div style={ tooltipStyles(theme) }>
{ content } { children }
</div> </div>
{/* <div style={footer(theme)}>{wikiLogo(theme)}</div> */} <div ref={arrowRef} style={ tooltipArrowStyles({
{/* {arrow(theme)} */} theme,
x: arrowX,
y: arrowY,
side: arrowPlacement
}) }></div>
</div> </div>
) )
} })

View File

@ -3,7 +3,6 @@ const absolutePath = ({ currentPath, basePath, relativePath }) => {
absolutePath.pop(); // remove current page name absolutePath.pop(); // remove current page name
absolutePath.unshift(basePath); absolutePath.unshift(basePath);
absolutePath.push(relativePath); absolutePath.push(relativePath);
console.log(absolutePath);
return absolutePath.join("/"); return absolutePath.join("/");
}; };

View File

@ -0,0 +1,16 @@
import { unified } from 'unified'
import rehypeParse from 'rehype-parse'
import find from 'unist-util-find'
import { toString } from 'hast-util-to-string'
// get first paragraph inside article's main tag
const documentExtract = (htmlString) => {
const hast = unified().use(rehypeParse).parse(htmlString);
const article = find(hast, (node) => node.tagName === "article");
const main = find(article, (node) => node.tagName === "main");
const paragraph = find(main, (node) => node.tagName === "p");
return toString(paragraph);
}
export default documentExtract;