Skip to content

Commit

Permalink
OPHJOD-293: Add Tooltip component
Browse files Browse the repository at this point in the history
  • Loading branch information
ketsappi committed Apr 26, 2024
1 parent a5b6195 commit 5efd6b8
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 0 deletions.
42 changes: 42 additions & 0 deletions lib/components/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';

import { Tooltip, TooltipProps } from './Tooltip';
import { TooltipTrigger } from './TooltipTrigger';
import { TooltipContent } from './TooltipContent';

const meta = {
component: Tooltip,
tags: ['autodocs'],
} satisfies Meta<typeof Tooltip>;

export default meta;

type Story = StoryObj<typeof meta>;

const parameters = {
design: {
type: 'figma',
url: 'https://www.figma.com/file/6M2LrpSCcB0thlFDaQAI2J/cx_jod_client?type=design&node-id=542-8065',
},
};

const DefaultStoryRender = (props: TooltipProps) => {
const [open, setOpen] = React.useState(true);
return (
<Tooltip {...props} open={open} onOpenChange={setOpen}>
<TooltipTrigger onClick={() => setOpen((v) => !v)}>Trigger</TooltipTrigger>
<TooltipContent>Lorem ipsum dolor sit amet, no vis verear commodo.</TooltipContent>
</Tooltip>
);
};

export const Default: Story = {
parameters: {
...parameters,
},
args: {},
render: (args) => {
return <DefaultStoryRender {...args} />;
},
};
23 changes: 23 additions & 0 deletions lib/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import type { Placement } from '@floating-ui/react';
import { useTooltip } from './utils';

export interface TooltipOptions {
initialOpen?: boolean;
placement?: Placement;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}

export interface TooltipProps extends TooltipOptions {
children?: React.ReactNode;
}

type ContextType = ReturnType<typeof useTooltip> | null;

export const TooltipContext = React.createContext<ContextType>(null);

export function Tooltip({ children, ...options }: TooltipProps) {
const tooltip = useTooltip(options);
return <TooltipContext.Provider value={tooltip}>{children}</TooltipContext.Provider>;
}
36 changes: 36 additions & 0 deletions lib/components/Tooltip/TooltipContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { FloatingArrow, FloatingPortal, useMergeRefs } from '@floating-ui/react';
import { useTooltipContext, ARROW_HEIGHT } from './utils';

export const TooltipContent = React.forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement>>(
function TooltipContent(props, propRef) {
const tooltipContext = useTooltipContext();
const ref = useMergeRefs([tooltipContext.refs.setFloating, propRef]);

if (!tooltipContext.open) {
return null;
}

return (
<FloatingPortal>
<div
className="max-w-[292px] rounded-[20px] bg-primary-gray px-6 py-5 text-body-sm text-white sm:text-body-md"
ref={ref}
style={{
...tooltipContext.floatingStyles,
}}
{...tooltipContext.getFloatingProps(props)}
>
{props.children}
<FloatingArrow
ref={tooltipContext.arrowRef}
context={tooltipContext.context}
className="fill-primary-gray"
width={ARROW_HEIGHT * 2}
height={ARROW_HEIGHT}
/>
</div>
</FloatingPortal>
);
},
);
36 changes: 36 additions & 0 deletions lib/components/Tooltip/TooltipTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { useMergeRefs } from '@floating-ui/react';
import { useTooltipContext } from './utils';

export const TooltipTrigger = React.forwardRef<
HTMLElement,
React.HTMLProps<HTMLElement> & {
asChild?: boolean;
children: React.ReactNode | { ref: React.ForwardedRef<HTMLElement> };
}
>(function TooltipTrigger({ children, asChild = false, ...props }, propRef) {
const context = useTooltipContext();
const childrenRef = (
children as {
ref: React.ForwardedRef<HTMLElement>;
}
).ref;
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);

if (asChild && React.isValidElement(children)) {
return React.cloneElement(
children,
context.getReferenceProps({
ref,
...props,
...(children as React.ReactElement<object>).props,
}),
);
}

return (
<button ref={ref} {...context.getReferenceProps(props)}>
{children}
</button>
);
});
87 changes: 87 additions & 0 deletions lib/components/Tooltip/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import {
arrow,
autoUpdate,
flip,
offset,
shift,
useFloating,
useFocus,
useDismiss,
useHover,
useInteractions,
useRole,
} from '@floating-ui/react';
import { TooltipContext, TooltipOptions } from './Tooltip';

export const ARROW_HEIGHT = 12;
export const GAP = 0;

export function useTooltip({
initialOpen = false,
placement = 'top',
open: controlledOpen,
onOpenChange: setControlledOpen,
}: TooltipOptions = {}) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);
const arrowRef = React.useRef(null);

const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;

const data = useFloating({
placement,
open,
onOpenChange: setOpen,
whileElementsMounted: autoUpdate,
middleware: [
offset(ARROW_HEIGHT + GAP),
flip({
crossAxis: placement.includes('-'),
fallbackAxisSideDirection: 'start',
padding: 5,
}),
shift({ padding: 5 }),
arrow({
element: arrowRef,
}),
],
});

const context = data.context;

const hover = useHover(context, {
move: false,
enabled: controlledOpen == null,
});

const focus = useFocus(context, {
enabled: controlledOpen == null,
});

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

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

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

export const useTooltipContext = () => {
const context = React.useContext(TooltipContext);

if (context == null) {
throw new Error('Tooltip components must be wrapped in <Tooltip />');
}

return context;
};
60 changes: 60 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"react": "^18"
},
"devDependencies": {
"@floating-ui/react": "^0.26.12",
"@headlessui/react": "^1.7.18",
"@storybook/addon-a11y": "^8.0.4",
"@storybook/addon-designs": "^8.0.0",
Expand Down

0 comments on commit 5efd6b8

Please sign in to comment.