Skip to content

Commit

Permalink
feat: tooltips for everyone
Browse files Browse the repository at this point in the history
  • Loading branch information
maxholman committed Nov 10, 2022
1 parent c6fedcd commit 343c5e2
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 12 deletions.
37 changes: 26 additions & 11 deletions lib/core.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ClassValue, clsx } from 'clsx';
import { createElement, PropsWithChildren } from 'react';
import { createElement, forwardRef, PropsWithChildren, ReactNode } from 'react';
import type { Merge } from 'type-fest';
import { marginVariants, paddingVariants } from './core.css.js';
import { Align, alignItems } from './layout.css.js';
import { Tooltip } from './tooltip.js';
import type { ReactHTMLAttributesHacked } from './types.js';

export type Space = 'none' | 'large' | 'small' | 'tiny' | 'huge' | 'standard';
Expand All @@ -19,22 +20,27 @@ export type BoxBasedComponentProps<
margin?: Space | undefined;
padding?: Space | undefined;
className?: ClassValue;
tooltip?: ReactNode;
// tone
// rounded
}
>
>;

export function Box<T extends keyof ReactHTMLAttributesHacked = 'div'>({
children,
component = 'div',
className,
align,
margin,
padding,
...props
}: BoxBasedComponentProps<T>) {
return createElement(
function BoxInner<T extends keyof ReactHTMLAttributesHacked = 'div'>(
{
children,
component = 'div',
className,
align,
margin,
padding,
tooltip,
...props
}: BoxBasedComponentProps<T>,
ref: React.ForwardedRef<ReactHTMLAttributesHacked[T]>,
) {
const el = createElement(
component,
{
...props,
Expand All @@ -44,7 +50,16 @@ export function Box<T extends keyof ReactHTMLAttributesHacked = 'div'>({
padding && paddingVariants[padding],
className,
),
ref,
},
children,
);

if (tooltip) {
return <Tooltip content={tooltip}>{el}</Tooltip>;
}

return el;
}

export const Box = forwardRef(BoxInner);
35 changes: 35 additions & 0 deletions lib/tooltip.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createVar, style } from '@vanilla-extract/css';
import { calc } from '@vanilla-extract/css-utils';
import { genericVars } from './design-system.css.js';
import { contrastSchemeVars } from './schemes/color.css.js';
import { hsl } from './utils.js';

export const tooltipClass = style({
position: 'absolute',
top: '0',
left: '0',
background: hsl(0, 0, contrastSchemeVars.fg.l),
color: hsl(0, 0, contrastSchemeVars.bg.l),
fontWeight: genericVars.text.weight.bold,
fontFamily: 'sans-serif',
padding: `${genericVars.space.tiny} ${genericVars.space.small}`,
borderRadius: genericVars.radius.standard,
fontSize: genericVars.text.size.small,
pointerEvents: 'none',
width: 'max-content',
});

export const arrowoffsetVar = createVar();

export const tooltipArrowStyle = style({
vars: {
[arrowoffsetVar]: calc(genericVars.space.tiny).negate().toString(),
},
position: 'absolute',
background: 'inherit',
height: genericVars.space.small,
borderRadius: genericVars.radius.large,
borderBottomRightRadius: '0',
aspectRatio: '1/1',
transform: 'rotate(45deg)',
});
144 changes: 144 additions & 0 deletions lib/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type { Placement } from '@floating-ui/react-dom-interactions';
import {
arrow,
autoUpdate,
flip,
offset,
shift,
Side,
useDismiss,
useFloating,
useFocus,
useHover,
useInteractions,
useRole,
} from '@floating-ui/react-dom-interactions';
import {
cloneElement,
FC,
isValidElement,
PropsWithChildren,
ReactNode,
useMemo,
useRef,
useState,
} from 'react';
import {
arrowoffsetVar,
tooltipArrowStyle,
tooltipClass,
} from './tooltip.css.js';

export type TooltipState = ReturnType<typeof useTooltipState>;

export function useTooltipState({
initialOpen = true,
placement = 'top',
}: {
initialOpen?: boolean;
placement?: Placement;
} = {}) {
const [open, setOpen] = useState(initialOpen);
const arrowRef = useRef(null);

const data = useFloating({
placement,
open,
onOpenChange: setOpen,
whileElementsMounted: autoUpdate,
middleware: [
offset(5),
flip(),
shift(),
// arrow should always be at the end, after shift()
// per https://floating-ui.com/docs/arrow#order
arrow({ element: arrowRef }),
],
});

const { context } = data;

const hover = useHover(context, { move: false });
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, { role: 'tooltip' });

const interactions = useInteractions([hover, focus, dismiss, role]);

return useMemo(
() => ({
open,
setOpen,
arrowRef,
...interactions,
...data,
}),
[open, setOpen, interactions, data],
);
}

export const Tooltip: FC<PropsWithChildren<{ content: ReactNode }>> = ({
content,
children,
}) => {
const {
placement,
reference,
floating,
strategy,
x,
y,
open,
arrowRef,
getReferenceProps,
getFloatingProps,
middlewareData: { arrow: { x: arrowX, y: arrowY } = {} },
} = useTooltipState();

if (!isValidElement(children)) {
return <>{children}</>;
}

const staticSide: Record<Side, Side> = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
};

const arrowPlacement = placement.split('-')[0] as Side;

return (
<>
{cloneElement(
children,
getReferenceProps({ ref: reference, ...children.props }),
)}
{open && (
<div
ref={floating}
{...getFloatingProps({
className: tooltipClass,
style: {
position: strategy,
top: y ?? 0,
left: x ?? 0,
visibility: x == null ? 'hidden' : 'visible',
},
})}
>
<div
ref={arrowRef}
className={tooltipArrowStyle}
style={{
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
[staticSide[arrowPlacement]]: arrowoffsetVar,
}}
/>
{content}
</div>
)}
</>
);
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"prepare": "npx patch-package"
},
"dependencies": {
"@floating-ui/react-dom": "^1.0.0",
"@floating-ui/react-dom-interactions": "^0.10.3",
"clsx": "^1.2.1"
},
"devDependencies": {
Expand Down
36 changes: 35 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,33 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"

"@floating-ui/core@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.0.1.tgz#00e64d74e911602c8533957af0cce5af6b2e93c8"
integrity sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==

"@floating-ui/dom@^1.0.0":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.0.4.tgz#cc0f2a03db7193b1b932b90d09c5c81235682a60"
integrity sha512-maYJRv+sAXTy4K9mzdv0JPyNW5YPVHrqtY90tEdI6XNpuLOP26Ci2pfwPsKBA/Wh4Z3FX5sUrtUFTdMYj9v+ug==
dependencies:
"@floating-ui/core" "^1.0.1"

"@floating-ui/react-dom-interactions@^0.10.3":
version "0.10.3"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.10.3.tgz#1d988aad169bf752b54c688db942f12e4fed61c5"
integrity sha512-UEHqdnzyoiWNU5az/tAljr9iXFzN18DcvpMqW+/cXz4FEhDEB1ogLtWldOWCujLerPBnSRocADALafelOReMpw==
dependencies:
"@floating-ui/react-dom" "^1.0.0"
aria-hidden "^1.1.3"

"@floating-ui/react-dom@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.0.0.tgz#e0975966694433f1f0abffeee5d8e6bb69b7d16e"
integrity sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg==
dependencies:
"@floating-ui/dom" "^1.0.0"

"@formatjs/[email protected]":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.13.0.tgz#df6db3cbee0182bbd2fd6217103781c802aee819"
Expand Down Expand Up @@ -920,6 +947,13 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==

aria-hidden@^1.1.3:
version "1.2.1"
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.1.tgz#ad8c1edbde360b454eb2bf717ea02da00bfee0f8"
integrity sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==
dependencies:
tslib "^2.0.0"

aria-query@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
Expand Down Expand Up @@ -3413,7 +3447,7 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==

tslib@^2.4.0:
tslib@^2.0.0, tslib@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
Expand Down

0 comments on commit 343c5e2

Please sign in to comment.