Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require a global tooltip provider #137

Merged
merged 2 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/components/Form/Controls/Action/Action.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const icons = {
};

import { ActionInput } from "./";
import { TooltipProvider } from "../../../Tooltip/TooltipProvider";

type Props = { invalid?: boolean } & React.ComponentProps<typeof ActionInput>;

Expand Down Expand Up @@ -88,7 +89,9 @@ export default {
},
},
render: ({ invalid, ...restArgs }) => (
<ActionInput data-invalid={invalid || undefined} {...restArgs} />
<TooltipProvider>
<ActionInput data-invalid={invalid || undefined} {...restArgs} />
</TooltipProvider>
),
args: {
placeholder: "",
Expand Down
29 changes: 17 additions & 12 deletions src/components/Form/Controls/Action/Action.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ import React from "react";
import ChatIcon from "@vector-im/compound-design-tokens/icons/chat.svg";

import { ActionInput } from "./Action";
import { TooltipProvider } from "../../../Tooltip/TooltipProvider";

describe("ActionInput", () => {
it("renders", () => {
const { asFragment } = render(
<ActionInput
Icon={ChatIcon}
actionLabel="Click me!"
onActionClick={() => {
console.log("clicked!");
}}
/>,
<TooltipProvider>
<ActionInput
Icon={ChatIcon}
actionLabel="Click me!"
onActionClick={() => {
console.log("clicked!");
}}
/>
</TooltipProvider>,
);
expect(asFragment()).toMatchSnapshot();
});
Expand All @@ -39,11 +42,13 @@ describe("ActionInput", () => {
const spy = vi.fn();

const { container } = render(
<ActionInput
Icon={ChatIcon}
actionLabel="Click me!"
onActionClick={spy}
/>,
<TooltipProvider>
<ActionInput
Icon={ChatIcon}
actionLabel="Click me!"
onActionClick={spy}
/>
</TooltipProvider>,
);

const actionBtn = getByLabelText(container, "Click me!");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Meta, StoryObj } from "@storybook/react";
import { PasswordInput } from "./";
import { within } from "@storybook/testing-library";
import { userEvent } from "@storybook/testing-library";
import { TooltipProvider } from "../../../Tooltip/TooltipProvider";

type Props = { invalid?: boolean } & React.ComponentProps<typeof PasswordInput>;

Expand Down Expand Up @@ -62,7 +63,9 @@ export default {
},
},
render: ({ invalid, ...restArgs }) => (
<PasswordInput data-invalid={invalid || undefined} {...restArgs} />
<TooltipProvider>
<PasswordInput data-invalid={invalid || undefined} {...restArgs} />
</TooltipProvider>
),
args: {
placeholder: "",
Expand Down
7 changes: 6 additions & 1 deletion src/components/Form/Controls/Password/Password.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ import { act, getByLabelText, render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { PasswordInput } from "./Password";
import { TooltipProvider } from "../../../Tooltip/TooltipProvider";

describe("PasswordControl", () => {
it("switches the input type", async () => {
const { container } = render(<PasswordInput defaultValue="p4ssw0rd!" />);
const { container } = render(
<TooltipProvider>
<PasswordInput defaultValue="p4ssw0rd!" />
</TooltipProvider>,
);

expect(container.querySelector("[type=password]")).toBeInTheDocument();
expect(container).toMatchSnapshot("invisible");
Expand Down
94 changes: 40 additions & 54 deletions src/components/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import React, { FC, ReactNode } from "react";
import { Meta, StoryFn } from "@storybook/react";

import { Tooltip as TooltipComponent } from "./Tooltip";
import { IconButton } from "../Button";

import UserIcon from "@vector-im/compound-design-tokens/icons/user-profile.svg";
import { TooltipProvider } from "./TooltipProvider";

export default {
title: "Tooltip",
Expand Down Expand Up @@ -72,14 +73,20 @@ export default {
},
decorators: [
(Story: StoryFn) => (
<div style={{ padding: 100 }}>
<Story />
</div>
<TooltipProvider>
<div style={{ padding: 100 }}>
<Story />
</div>
</TooltipProvider>
),
],
} as Meta<typeof TooltipComponent>;

const TemplateSide: StoryFn<typeof TooltipComponent> = () => (
interface LayoutProps {
children: ReactNode;
}

const Layout: FC<LayoutProps> = ({ children }) => (
<div
style={{
display: "flex",
Expand All @@ -88,67 +95,46 @@ const TemplateSide: StoryFn<typeof TooltipComponent> = () => (
alignItems: "center",
}}
>
<TooltipComponent open={true} side="top" label="@bob:example.org">
<IconButton>
<UserIcon />
</IconButton>
</TooltipComponent>
<TooltipComponent open={true} side="right" label="@bob:example.org">
<IconButton>
<UserIcon />
</IconButton>
</TooltipComponent>
<TooltipComponent open={true} side="bottom" label="@bob:example.org">
<IconButton>
<UserIcon />
</IconButton>
</TooltipComponent>
<TooltipComponent open={true} side="left" label="@bob:example.org">
<IconButton>
<UserIcon />
</IconButton>
</TooltipComponent>
{children}
</div>
);

const TemplateSide: StoryFn<typeof TooltipComponent> = () => (
<Layout>
{(["top", "right", "bottom", "left"] as const).map((side) => (
<TooltipComponent key={side} open side={side} label="@bob:example.org">
<IconButton>
<UserIcon />
</IconButton>
</TooltipComponent>
))}
</Layout>
);

export const Side = TemplateSide.bind({});
Side.args = {};

const TemplateAlign: StoryFn<typeof TooltipComponent> = () => (
<div
style={{
display: "flex",
gap: "50px",
flexDirection: "column",
alignItems: "center",
}}
>
<Layout>
<TooltipComponent open={true} align="center" label="Copy" caption="⌘ + C">
<IconButton>
<UserIcon />
</IconButton>
</TooltipComponent>
<TooltipComponent
open={true}
align="start"
label="@bob:example.org"
caption="⌘ + C"
>
<IconButton>
<UserIcon />
</IconButton>
</TooltipComponent>
<TooltipComponent
open={true}
align="end"
label="@bob:example.org"
caption="⌘ + C"
>
<IconButton>
<UserIcon />
</IconButton>
</TooltipComponent>
</div>
{(["start", "end"] as const).map((align) => (
<TooltipComponent
key={align}
open
align={align}
label="@bob:example.org"
caption="⌘ + C"
>
<IconButton>
<UserIcon />
</IconButton>
</TooltipComponent>
))}
</Layout>
);

export const Align = TemplateAlign.bind({});
Expand Down
69 changes: 30 additions & 39 deletions src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,7 @@ limitations under the License.
*/

import React, { PropsWithChildren } from "react";
import {
Root,
Trigger,
Portal,
Content,
Arrow,
Provider,
} from "@radix-ui/react-tooltip";
import { Root, Trigger, Portal, Content, Arrow } from "@radix-ui/react-tooltip";

import styles from "./Tooltip.module.css";
import classNames from "classnames";
Expand Down Expand Up @@ -101,37 +94,35 @@ export const Tooltip = ({
open,
}: PropsWithChildren<TooltipProps>): JSX.Element => {
return (
<Provider>
<Root open={open} delayDuration={isTriggerInteractive ? 300 : 0}>
<Trigger asChild>
{isTriggerInteractive ? (
children
) : (
<span tabIndex={nonInteractiveTriggerTabIndex}>{children}</span>
<Root open={open} delayDuration={isTriggerInteractive ? 300 : 0}>
<Trigger asChild>
{isTriggerInteractive ? (
children
) : (
<span tabIndex={nonInteractiveTriggerTabIndex}>{children}</span>
)}
</Trigger>
<Portal>
<Content
side={side}
align={align}
onEscapeKeyDown={onEscapeKeyDown}
onPointerDownOutside={onPointerDownOutside}
className={styles.tooltip}
>
{label}
{/* Forcing dark theme, so that we have the correct contrast when
using the text color secondary on a solid dark background.
This is temporary and should only remain until we figure out
the approach to on-solid tokens */}
{caption && (
<span className={classNames(styles.caption, "cpd-theme-dark")}>
{caption}
</span>
)}
</Trigger>
<Portal>
<Content
side={side}
align={align}
onEscapeKeyDown={onEscapeKeyDown}
onPointerDownOutside={onPointerDownOutside}
className={styles.tooltip}
>
{label}
{/* Forcing dark theme, so that we have the correct contrast when
using the text color secondary on a solid dark background.
This is temporary and should only remain until we figure out
the approach to on-solid tokens */}
{caption && (
<span className={classNames(styles.caption, "cpd-theme-dark")}>
{caption}
</span>
)}
<Arrow width={10} height={6} className={styles.arrow} />
</Content>
</Portal>
</Root>
</Provider>
<Arrow width={10} height={6} className={styles.arrow} />
</Content>
</Portal>
</Root>
);
};
28 changes: 28 additions & 0 deletions src/components/Tooltip/TooltipProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
Copyright 2024 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { Provider } from "@radix-ui/react-tooltip";
import { FC, ReactNode } from "react";

interface TooltipProviderProps {
children: ReactNode;
}

/**
* Provides global functionality to your tooltips. You must wrap your
* application in this component for tooltips to function.
*/
export const TooltipProvider: FC<TooltipProviderProps> = Provider;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export { Search } from "./components/Search/Search";
export { Separator } from "./components/Separator/Separator";
export { ToggleMenuItem } from "./components/Menu/ToggleMenuItem";
export { Tooltip } from "./components/Tooltip/Tooltip";
export { TooltipProvider } from "./components/Tooltip/TooltipProvider";

export {
TextControl,
Expand Down
Loading