Skip to content

Commit

Permalink
feat: tooltip component
Browse files Browse the repository at this point in the history
  • Loading branch information
mogusbi-motech committed Feb 20, 2023
1 parent f5338aa commit 9d15aba
Show file tree
Hide file tree
Showing 8 changed files with 1,561 additions and 98 deletions.
43 changes: 23 additions & 20 deletions packages/project-tailwind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,51 +25,54 @@
"test-ci": "jest --coverage --runInBand"
},
"dependencies": {
"@headlessui/react": "^1.7.11",
"@heroicons/react": "^2.0.15",
"clsx": "^1.2.1",
"@floating-ui/react": "0.19.2",
"@headlessui/react": "1.7.11",
"@heroicons/react": "2.0.15",
"clsx": "1.2.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-polymorphic-box": "^3.0.3"
"react-polymorphic-box": "3.0.3"
},
"devDependencies": {
"@babel/core": "7.20.12",
"@mdx-js/react": "^1.6.22",
"@mdx-js/react": "1.6.22",
"@motech-development/eslint-config-motech-react": "workspace:*",
"@motech-development/prettier-motech-config": "workspace:*",
"@motech-development/semantic-release": "workspace:*",
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "23.0.7",
"@rollup/plugin-node-resolve": "15.0.1",
"@storybook/addon-actions": "^6.5.16",
"@storybook/addon-docs": "^6.5.16",
"@storybook/addon-essentials": "^6.5.16",
"@storybook/addon-interactions": "^6.5.16",
"@storybook/addon-links": "^6.5.16",
"@storybook/builder-webpack5": "^6.5.16",
"@storybook/manager-webpack5": "^6.5.16",
"@storybook/react": "^6.5.16",
"@storybook/addon-actions": "6.5.16",
"@storybook/addon-docs": "6.5.16",
"@storybook/addon-essentials": "6.5.16",
"@storybook/addon-interactions": "6.5.16",
"@storybook/addon-links": "6.5.16",
"@storybook/builder-webpack5": "6.5.16",
"@storybook/manager-webpack5": "6.5.16",
"@storybook/react": "6.5.16",
"@storybook/storybook-deployer": "2.8.16",
"@storybook/testing-library": "^0.0.13",
"@storybook/testing-library": "0.0.13",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "13.5.0",
"@types/jest": "29.4.0",
"@types/react": "17.0.53",
"@types/react-dom": "17.0.18",
"autoprefixer": "^10.4.13",
"babel-loader": "^8.3.0",
"autoprefixer": "10.4.13",
"babel-loader": "8.3.0",
"eslint": "8.33.0",
"jest": "29.4.2",
"jest-environment-jsdom": "^29.4.2",
"postcss": "^8.4.21",
"jest-environment-jsdom": "29.4.2",
"jsdom-testing-mocks": "1.7.0",
"postcss": "8.4.21",
"prettier": "2.3.0",
"rimraf": "3.0.2",
"rollup": "2.79.1",
"rollup-plugin-exclude-dependencies-from-bundle": "1.1.23",
"rollup-plugin-postcss": "^4.0.2",
"tailwindcss": "^3.2.6",
"rollup-plugin-postcss": "4.0.2",
"snapshot-diff": "0.10.0",
"tailwindcss": "3.2.6",
"typescript": "4.9.5"
},
"peerDependencies": {
Expand Down
243 changes: 243 additions & 0 deletions packages/project-tailwind/src/components/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import {
arrow,
autoUpdate,
flip,
MiddlewareData,
offset,
Placement,
shift,
Side,
useDismiss,
useFloating,
useFocus,
useHover,
useInteractions,
useRole,
} from '@floating-ui/react';
import { ComponentPropsWithoutRef, ReactNode, useRef, useState } from 'react';
import { Themes, TTheme, useTailwind } from '../utilities/tailwind';

/** Default legth of time to display tooltip after moving away from parent */
const DEFAULT_TIME = 1000;

/** Arrow box dimension */
const BOX_SIZE = 8;

/** Arrow box's hypotenuse length */
const FLOATING_OFFSET = Math.sqrt(2 * BOX_SIZE ** 2) / 2;

/**
* Creates inline styles for arrows
*
* @param element - Arrow from middleware
*
* @param placement - Arrow placement
*
* @returns Arrow inline styles
*/
function createArrowStyles(
element: MiddlewareData['arrow'],
placement: Placement,
) {
let styles = {
bottom: '',
left: '',
right: '',
top: '',
};

if (element) {
const { x, y } = element;

const side = placement.split('-')[0];

/** Position to apply generated style */
const xy = {
bottom: 'top',
left: 'right',
right: 'left',
top: 'bottom',
}[side] as string;

styles = {
...styles,
left: xy === 'right' ? '' : `${x ?? BOX_SIZE}px`,
top: xy === 'bottom' ? '' : `${y ?? BOX_SIZE}px`,
[xy]: -BOX_SIZE / 2,
};
}

return styles;
}

/** Tooltip component properties */
export interface ITooltipProps extends ComponentPropsWithoutRef<'div'> {
/** Content to be displayed */
content: ReactNode;

/** Element to output tooltip relative to */
parent: ReactNode;

/** Where tooltip should be places */
placement?: Side;

/** Length of time to keep tooltip visible in milliseconds */
time?: number;

/** Component theme */
theme?: TTheme;
}

/**
* Display information when hovering over a component
*
* @param props - Component props
*
* @returns Tooltip component
*/
export function Tooltip({
className,
content,
id,
parent,
placement = 'bottom',
time = DEFAULT_TIME,
theme = Themes.PRIMARY,
...rest
}: ITooltipProps) {
const arrowRef = useRef(null);

const { createStyles } = useTailwind(theme);

const [isOpen, setIsOpen] = useState(false);

const {
context,
middlewareData,
placement: floatingPlacement,
refs,
strategy,
x,
y,
} = useFloating({
middleware: [
flip(),
offset(FLOATING_OFFSET),
shift(),
arrow({
element: arrowRef,
}),
],
onOpenChange: setIsOpen,
open: isOpen,
placement,
whileElementsMounted: autoUpdate,
});

const hover = useHover(context, {
delay: {
close: time,
},
move: false,
});

const focus = useFocus(context);

const dismiss = useDismiss(context);

const role = useRole(context, {
role: 'tooltip',
});

const { getFloatingProps, getReferenceProps } = useInteractions([
hover,
focus,
dismiss,
role,
]);

const tooltipStyles = createStyles({
classNames: [className],
theme: {
danger: ['bg-red-600 text-red-50'],
primary: ['bg-blue-600 text-blue-50'],
secondary: ['bg-gray-100 text-gray-600'],
success: ['bg-green-600 text-green-50'],
warning: ['bg-yellow-600 text-yellow-50'],
},
});

const innerTooltipStyles = createStyles({
classNames: [
'px-1 text-sm font-display whitespace-nowrap',
{
'border-b-2': placement === 'top',
'border-l-2': placement === 'right',
'border-r-2': placement === 'left',
'border-t-2': placement === 'bottom',
},
],
theme: {
danger: ['border-red-700'],
primary: ['border-blue-700'],
secondary: ['border-gray-200'],
success: ['border-green-700'],
warning: ['border-yellow-700'],
},
});

const tooltipInlineStyles = {
left: x ?? 0,
position: strategy,
top: y ?? 0,
width: 'max-content',
};

const arrowStyles = createStyles({
classNames: ['absolute w-2 h-2 rotate-45 -z-10 pointer-events-none'],
theme: {
danger: ['bg-red-700'],
primary: ['bg-blue-700'],
secondary: ['bg-gray-200'],
success: ['bg-green-700'],
warning: ['bg-yellow-700'],
},
});

const arrowInlineStyles = createArrowStyles(
middlewareData.arrow,
floatingPlacement,
);

return (
<>
<div
className="inline-block"
data-testid="tooltip-parent-element"
ref={refs.setReference}
{...getReferenceProps()}
>
{parent}
</div>

{isOpen && (
<div
id={id}
className={tooltipStyles}
ref={refs.setFloating}
style={tooltipInlineStyles}
{...getFloatingProps()}
{...rest}
>
<div className={innerTooltipStyles}>{content}</div>

<div
className={arrowStyles}
ref={arrowRef}
style={arrowInlineStyles}
/>
</div>
)}
</>
);
}
Loading

0 comments on commit 9d15aba

Please sign in to comment.