@import url('https://fonts.googleapis.com/css?family=Encode+Sans+Expanded');
html,body {
font-family: 'Encode Sans Expanded', sans-serif;
height: 100%;
margin: 1rem;
margin-top: 25vh;
}
u.tip {
text-decoration: underline;
text-decoration-style: dashed;
text-decoration-color: #69C;
cursor: default;
}
.tooltip {
display: inline-block;
background-color: #444;
color: #fff;
border-radius: 6px;
padding: .25rem .5rem;
margin-top: 10px;
font-size: .75rem;
cursor: default;
&::after {
content: '';
position: absolute;
top: -5px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
background-color: #444;
width: 10px;
height: 10px;
}
}
View Compiled
const { Component } = React;
const { render } = ReactDOM;
const cx = classNames;
// const useOutsideClick = (ref, callback) => {
// const handleClick = e => {
// if (ref.current && !ref.current.contains(e.target)) {
// callback();
// }
// };
// useEffect(() => {
// document.addEventListener("click", handleClick);
// return () => {
// document.removeEventListener("click", handleClick);
// };
// });
// };
const composeCallbacks = (...callbacks) => {
return (...args) => {
const fns = callbacks.filter(Boolean);
for (const callback of fns) callback(...args);
};
};
const mergeRefs = (...refs) => {
const filteredRefs = refs.filter(Boolean);
if (!filteredRefs.length) return null;
if (filteredRefs.length === 0) return filteredRefs[0];
return inst => {
for (const ref of filteredRefs) {
if (typeof ref === 'function') {
ref(inst);
} else if (ref) {
ref.current = inst;
}
}
};
};
const Tooltip = ({ text, children }) => {
const [active, setActive] = React.useState(false);
const [{ top, left }, setPosition] = React.useState({ top: 0, left: 0 });
const trigger = React.useRef(null);
const tip = React.useRef(null);
const show = () => setActive(true);
const hide = () => setActive(false);
const child = typeof children === 'string' ?
<span>{children}</span> :
React.Children.only(children);
React.useEffect(() => {
if (active && (trigger && trigger.current) && (tip && tip.current)) {
const triggerEl = trigger.current.getBoundingClientRect();
const tipEl = tip.current.getBoundingClientRect();
setPosition({
top: (triggerEl.y + window.pageYOffset) + triggerEl.height,
left: (triggerEl.x + window.pageXOffset) + ((triggerEl.width - tipEl.width)/2)
});
}
}, [active, trigger, tip]);
return (
<>
{ React.cloneElement(child, {
...child.props,
ref: mergeRefs(child.ref, trigger),
onMouseEnter: child.props['onMouseEnter'] ?
composeCallbacks(child.props.onMouseEnter, show) :
show,
onMouseLeave: child.props['onMouseLeave'] ?
composeCallbacks(child.props.onMouseLeave, hide) :
hide
})}
{ active && ReactDOM.createPortal(
<div className="tooltip" ref={tip} style={{ position: 'absolute', top, left }}>
{text}
</div>,
document.body)
}
</>
);
}
const App = () => {
return (
<div>
A tooltip{" "}
<Tooltip text="a tooltip">
<u className="tip">hover me</u>
</Tooltip>
{" "}
within a sentence.
</div>
);
};
render(
<App/>
, document.querySelector('#root'))
View Compiled