Skip to content

Commit

Permalink
Tooltip Component (#99)
Browse files Browse the repository at this point in the history
* feat: initial tooltip component

* feat: add example app demos

* feat: use render function isopen for tooltip arrow to display properly

* refactor: update typos in spec file

* chore: remove unecessary props in default story for cleanliness

* chore: add changeset

* refactor: modify spacing

* chore: add default props for best practices and remove unused await in spec file
  • Loading branch information
Twonarly1 authored Sep 11, 2023
1 parent 9d1c527 commit dfd8aeb
Show file tree
Hide file tree
Showing 15 changed files with 313 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-carrots-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@animareflection/ui": minor
---

Add `Tooltip` component
2 changes: 2 additions & 0 deletions examples/next/src/app/client/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
SliderDemo,
TabsDemo,
ToggleDemo,
TooltipDemo,
} from "components";

const ClientPage = () => (
Expand All @@ -40,6 +41,7 @@ const ClientPage = () => (
<InputDemo />
<NumberInputDemo />
<FlyoutDemo />
<TooltipDemo />
</Flex>
);

Expand Down
19 changes: 19 additions & 0 deletions examples/next/src/components/TooltipDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Button, Tooltip } from "@animareflection/ui/client";

import { Wrapper } from "components";

const FlyoutDemo = () => {
return (
<Wrapper title="Tooltip">
<Tooltip
positioning={{
placement: "top",
}}
trigger={<Button>Open Tooltip</Button>}
content="Tooltip Title"
/>
</Wrapper>
);
};

export default FlyoutDemo;
1 change: 1 addition & 0 deletions examples/next/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export { default as SliderDemo } from "./SliderDemo";
export { default as SpinnerDemo } from "./SpinnerDemo";
export { default as TabsDemo } from "./TabsDemo";
export { default as ToggleDemo } from "./ToggleDemo";
export { default as TooltipDemo } from "./TooltipDemo";
export { default as Wrapper } from "./Wrapper";
2 changes: 2 additions & 0 deletions examples/vite-react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
SpinnerDemo,
TabsDemo,
ToggleDemo,
TooltipDemo,
} from "components";

import "main.css";
Expand Down Expand Up @@ -105,6 +106,7 @@ const App = () => (
<InputDemo />
<NumberInputDemo />
<FlyoutDemo />
<TooltipDemo />

<Hide below="md">
<Text color="brand.primary.500" fontSize="3xl" fontWeight="bold">
Expand Down
19 changes: 19 additions & 0 deletions examples/vite-react/src/components/TooltipDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Button, Tooltip } from "@animareflection/ui/client";

import { Wrapper } from "components";

const FlyoutDemo = () => {
return (
<Wrapper title="Tooltip">
<Tooltip
positioning={{
placement: "top",
}}
trigger={<Button>Open Tooltip</Button>}
content="Tooltip Title"
/>
</Wrapper>
);
};

export default FlyoutDemo;
1 change: 1 addition & 0 deletions examples/vite-react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export { default as SliderDemo } from "./SliderDemo";
export { default as SpinnerDemo } from "./SpinnerDemo";
export { default as TabsDemo } from "./TabsDemo";
export { default as ToggleDemo } from "./ToggleDemo";
export { default as TooltipDemo } from "./TooltipDemo";
export { default as Wrapper } from "./Wrapper";
33 changes: 33 additions & 0 deletions src/components/client/core/Tooltip/Tooltip.recipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { defineSlotRecipe } from "@pandacss/dev";

export const tooltipRecipe = defineSlotRecipe({
className: "tooltip",
description: "The styles for the Tooltip component",
slots: ["positioner", "content", "arrow", "arrowTip"],
base: {
positioner: {
position: "relative",
},
content: {
background: "bg.default",
boxShadow: "lg",
borderRadius: "md",
borderWidth: "1px",
borderColor: "border.default",
fontWeight: "semibold",
px: "3",
py: "2",
textStyle: "sm",
color: "fg.default",
},
arrow: {
"--arrow-size": "var(--sizes-3)",
"--arrow-background": "var(--colors-bg-default)",
},
arrowTip: {
borderTopWidth: "1px",
borderColor: "border.default",
borderLeftWidth: "1px",
},
},
});
33 changes: 33 additions & 0 deletions src/components/client/core/Tooltip/Tooltip.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect } from "@storybook/jest";
import { screen, userEvent, within } from "@storybook/testing-library";

import type { ReactRenderer } from "@storybook/react";
import type { PlayFunctionContext, Renderer } from "@storybook/types";

/**
* Tooltip testing suite.
*/
export const tooltipState = async <R extends Renderer = ReactRenderer>({
canvasElement,
step,
}: PlayFunctionContext<R>) => {
const canvas = within(canvasElement as HTMLElement);

const hoverButton = canvas.getByText("Open Tooltip");

await step("It should open tooltip on button hover", async () => {
await userEvent.hover(hoverButton);

const tooltipTitle = screen.getByText("Tooltip Title");

await expect(tooltipTitle).toBeVisible();
});

await step("It should close tooltip when the hover ends", async () => {
await userEvent.unhover(hoverButton);

const tooltipTitle = screen.getByText("Tooltip Title");

await expect(tooltipTitle).not.toBeVisible();
});
};
75 changes: 75 additions & 0 deletions src/components/client/core/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { tooltipState } from "./Tooltip.spec";
import { Button, Tooltip } from "components/client";
import { Flex, Grid } from "generated/panda/jsx";

import type { Meta, StoryObj } from "@storybook/react";

type Story = StoryObj<typeof Tooltip>;

type Placement =
| "top"
| "top-start"
| "top-end"
| "bottom"
| "bottom-start"
| "bottom-end"
| "left"
| "left-start"
| "left-end"
| "right"
| "right-start"
| "right-end";

const TooltipTemplate = ({ placement }: { placement: Placement }) => (
<Tooltip
positioning={{
placement: placement,
}}
trigger={<Button minW={32}>{placement}</Button>}
content="Tooltip Title"
/>
);

export const Default: Story = {
render: () => (
<Tooltip trigger={<Button>Open Tooltip</Button>} content="Tooltip Title" />
),
};

export const Placement: Story = {
render: () => (
<Flex h="screen" w="full" justify="center" align="center">
<Grid columns={{ base: 2, sm: 3 }} justifyContent="center">
<TooltipTemplate placement="bottom-start" />
<TooltipTemplate placement="bottom" />
<TooltipTemplate placement="bottom-end" />
<TooltipTemplate placement="left-start" />
<TooltipTemplate placement="left" />
<TooltipTemplate placement="left-end" />
<TooltipTemplate placement="right-start" />
<TooltipTemplate placement="right" />
<TooltipTemplate placement="right-end" />
<TooltipTemplate placement="top-start" />
<TooltipTemplate placement="top" />
<TooltipTemplate placement="top-end" />
</Grid>
</Flex>
),
};

export const TooltipState: Story = {
...Default,
play: tooltipState,
name: "[TEST] Tooltip State",
tags: ["test"],
};

// TODO remove explicit type annotation, required due to `pnpm` bug (and therefore Yarn with `pnpm` linker); https://github.com/microsoft/TypeScript/issues/47663
const meta: Meta<typeof Tooltip> = {
title: "Components/Client/Core/Tooltip",
component: Tooltip,
tags: ["autodocs"],
decorators: [(Story) => <Story />],
} satisfies Meta<typeof Tooltip>;

export default meta;
61 changes: 61 additions & 0 deletions src/components/client/core/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Portal } from "@ark-ui/react";

import {
Tooltip as PrimitiveTooltip,
TooltipArrow,
TooltipArrowTip,
TooltipContent,
TooltipPositioner,
TooltipTrigger,
} from "components/primitives";
import { tooltip } from "generated/panda/recipes";
import { useIsMounted } from "lib/hooks";

import type { TooltipProps } from "components/primitives";
import type { ReactNode } from "react";

export interface Props extends TooltipProps {
trigger: ReactNode;
content: ReactNode;
}

/**
* Core UI tooltip.
*/
const Tooltip = ({
trigger,
content,
openDelay = 0,
closeDelay = 0,
...rest
}: Props) => {
const classNames = tooltip();

const isMounted = useIsMounted();

if (!isMounted) return null;

return (
<PrimitiveTooltip openDelay={openDelay} closeDelay={closeDelay} {...rest}>
{({ isOpen }) => (
<>
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
<Portal>
<TooltipPositioner className={classNames.positioner}>
{isOpen && (
<TooltipArrow className={classNames.arrow}>
<TooltipArrowTip className={classNames.arrowTip} />
</TooltipArrow>
)}
<TooltipContent className={classNames.content}>
{content}
</TooltipContent>
</TooltipPositioner>
</Portal>
</>
)}
</PrimitiveTooltip>
);
};

export default Tooltip;
2 changes: 2 additions & 0 deletions src/components/client/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { default as NumberInput } from "./NumberInput/NumberInput";
export { default as Slider } from "./Slider/Slider";
export { default as Tabs } from "./Tabs/Tabs";
export { default as Toggle } from "./Toggle/Toggle";
export { default as Tooltip } from "./Tooltip/Tooltip";

export type {
Props as AccordionProps,
Expand All @@ -34,3 +35,4 @@ export type { Props as NumberInputProps } from "./NumberInput/NumberInput";
export type { Props as SliderProps, SliderMarkerRecord } from "./Slider/Slider";
export type { Props as TabsProps, TabRecord } from "./Tabs/Tabs";
export type { Props as ToggleProps } from "./Toggle/Toggle";
export type { Props as TooltipProps } from "./Tooltip/Tooltip";
42 changes: 42 additions & 0 deletions src/components/primitives/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
Tooltip as ArkTooltip,
TooltipArrow as ArkTooltipArrow,
TooltipArrowTip as ArkTooltipArrowTip,
TooltipContent as ArkTooltipContent,
TooltipPositioner as ArkTooltipPositioner,
TooltipTrigger as ArkTooltipTrigger,
} from "@ark-ui/react";

import { panda } from "generated/panda/jsx";

import type {
TooltipProps as ArkTooltipProps,
TooltipArrowProps as ArkTooltipArrowProps,
TooltipArrowTipProps as ArkTooltipArrowTipProps,
TooltipContentProps as ArkTooltipContentProps,
TooltipPositionerProps as ArkTooltipPositionerProps,
TooltipTriggerProps as ArkTooltipTriggerProps,
} from "@ark-ui/react";

/**
* Core UI tooltip primitives.
*/
export type TooltipProps = ArkTooltipProps;
const Tooltip = panda(ArkTooltip);

export type TooltipArrowProps = ArkTooltipArrowProps;
export const TooltipArrow = panda(ArkTooltipArrow);

export type TooltipArrowTipProps = ArkTooltipArrowTipProps;
export const TooltipArrowTip = panda(ArkTooltipArrowTip);

export type TooltipContentProps = ArkTooltipContentProps;
export const TooltipContent = panda(ArkTooltipContent);

export type TooltipPositionerProps = ArkTooltipPositionerProps;
export const TooltipPositioner = panda(ArkTooltipPositioner);

export type TooltipTriggerProps = ArkTooltipTriggerProps;
export const TooltipTrigger = panda(ArkTooltipTrigger);

export default Tooltip;
18 changes: 16 additions & 2 deletions src/components/primitives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,14 @@ export {
ToggleLabel,
ToggleThumb,
} from "./Toggle/Toggle";

export {
default as Tooltip,
TooltipArrow,
TooltipArrowTip,
TooltipContent,
TooltipPositioner,
TooltipTrigger,
} from "./Tooltip/Tooltip";
export type {
AccordionProps,
AccordionItemProps,
Expand Down Expand Up @@ -155,7 +162,6 @@ export type {
FlyoutTitleProps,
FlyoutTriggerProps,
} from "./Flyout/Flyout";

export type {
SliderProps,
SliderControlProps,
Expand All @@ -180,3 +186,11 @@ export type {
ToggleLabelProps,
ToggleThumbProps,
} from "./Toggle/Toggle";
export type {
TooltipProps,
TooltipArrowProps,
TooltipArrowTipProps,
TooltipContentProps,
TooltipPositionerProps,
TooltipTriggerProps,
} from "./Tooltip/Tooltip";
Loading

0 comments on commit dfd8aeb

Please sign in to comment.