[site/components][f]: floatingui tooltip + arrow
This commit is contained in:
parent
0b65f7d404
commit
5290e24d95
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
|
||||||
|
|
@ -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("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
Loading…
Reference in New Issue