Skip to content

refactor(site): make HelpTooltip easier to reuse and compose #11242

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

Merged
merged 12 commits into from
Dec 19, 2023
Next Next commit
Refactor help tooltip
  • Loading branch information
BrunoQuaresma committed Dec 15, 2023
commit ce7394ca5d819b4f2c28ee73e39bfe8ef7ce5207
17 changes: 11 additions & 6 deletions site/src/components/ActiveUserChart/ActiveUserChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
HelpTooltip,
HelpTooltipTitle,
HelpTooltipText,
HelpTooltipContent,
HelpTooltipTrigger,
} from "components/HelpTooltip/HelpTooltip";
import dayjs from "dayjs";
import { useTheme } from "@emotion/react";
Expand Down Expand Up @@ -139,12 +141,15 @@ export const ActiveUsersTitle: FC = () => {
return (
<div css={{ display: "flex", alignItems: "center", gap: 8 }}>
Active Users
<HelpTooltip size="small">
<HelpTooltipTitle>How do we calculate active users?</HelpTooltipTitle>
<HelpTooltipText>
When a connection is initiated to a user&apos;s workspace they are
considered an active user. e.g. apps, web terminal, SSH
</HelpTooltipText>
<HelpTooltip>
<HelpTooltipTrigger size="small" />
<HelpTooltipContent>
<HelpTooltipTitle>How do we calculate active users?</HelpTooltipTitle>
<HelpTooltipText>
When a connection is initiated to a user&apos;s workspace they are
considered an active user. e.g. apps, web terminal, SSH
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
</div>
);
Expand Down
190 changes: 56 additions & 134 deletions site/src/components/HelpTooltip/HelpTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,114 +1,78 @@
import Link from "@mui/material/Link";
// This is used as base for the main HelpTooltip component
// eslint-disable-next-line no-restricted-imports -- Read above
import Popover, { type PopoverProps } from "@mui/material/Popover";
import HelpIcon from "@mui/icons-material/HelpOutline";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import {
createContext,
useContext,
useRef,
useState,
type FC,
type PropsWithChildren,
type HTMLAttributes,
type ReactNode,
forwardRef,
ComponentProps,
} from "react";
import { Stack } from "components/Stack/Stack";
import type { CSSObject } from "@emotion/css";
import { type CSSObject } from "@emotion/css";
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
import { type ClassName, useClassName } from "hooks/useClassName";
import {
Popover,
PopoverContent,
PopoverTrigger,
usePopover,
} from "components/Popover/Popover";

type Icon = typeof HelpIcon;

type Size = "small" | "medium";

export const HelpTooltipContext = createContext<
{ open: boolean; onClose: () => void } | undefined
>(undefined);

const useHelpTooltip = () => {
const helpTooltipContext = useContext(HelpTooltipContext);

if (!helpTooltipContext) {
throw new Error(
"This hook should be used in side of the HelpTooltipContext.",
);
}
export const HelpTooltipIcon = HelpIcon;

return helpTooltipContext;
export const HelpTooltip: FC<ComponentProps<typeof Popover>> = (props) => {
return <Popover mode="hover" {...props} />;
};

interface HelpPopoverProps extends PopoverProps {
onOpen: () => void;
onClose: () => void;
}

export const HelpPopover: FC<HelpPopoverProps> = ({
onOpen,
onClose,
children,
...props
}) => {
const popover = useClassName(classNames.popover, []);
const paper = useClassName(classNames.paper, []);
export const HelpTooltipContent = (
props: ComponentProps<typeof PopoverContent>,
) => {
const theme = useTheme();

return (
<Popover
className={popover}
classes={{ paper }}
onClose={onClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
PaperProps={{
onMouseEnter: onOpen,
onMouseLeave: onClose,
}}
<PopoverContent
{...props}
>
{children}
</Popover>
css={{
"& .MuiPaper-root": {
fontSize: 14,
width: 304,
padding: 20,
color: theme.palette.text.secondary,
},
}}
/>
);
};

export interface HelpTooltipProps {
// Useful to test on storybook
open?: boolean;
type HelpTooltipTriggerProps = Omit<
HTMLAttributes<HTMLButtonElement>,
"size"
> & {
size?: Size;
icon?: Icon;
buttonStyles?: Interpolation<Theme>;
iconStyles?: Interpolation<Theme>;
children?: ReactNode;
}
};

export const HelpTooltip: FC<HelpTooltipProps> = ({
children,
open = false,
size = "medium",
icon: Icon = HelpIcon,
buttonStyles,
iconStyles,
}) => {
export const HelpTooltipTrigger = forwardRef<
HTMLButtonElement,
HelpTooltipTriggerProps
>((props, ref) => {
const theme = useTheme();
const anchorRef = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState(open);
const id = isOpen ? "help-popover" : undefined;

const onClose = () => {
setIsOpen(false);
};
const {
size = "medium",
children = <HelpTooltipIcon />,
...buttonProps
} = props;

return (
<>
<PopoverTrigger>
<button
ref={anchorRef}
aria-describedby={id}
{...buttonProps}
aria-label="More info"
ref={ref}
css={[
css`
display: flex;
Expand All @@ -126,45 +90,19 @@ export const HelpTooltip: FC<HelpTooltipProps> = ({
&:hover {
opacity: 0.75;
}

& svg {
width: ${getIconSpacingFromSize(size)}px;
height: ${getIconSpacingFromSize(size)}px;
}
`,
buttonStyles,
]}
onClick={(event) => {
event.stopPropagation();
setIsOpen(true);
}}
onMouseEnter={() => {
setIsOpen(true);
}}
onMouseLeave={() => {
setIsOpen(false);
}}
aria-label="More info"
>
<Icon
css={[
{
width: theme.spacing(getIconSpacingFromSize(size)),
height: theme.spacing(getIconSpacingFromSize(size)),
},
iconStyles,
]}
/>
{children}
</button>
<HelpPopover
id={id}
open={isOpen}
anchorEl={anchorRef.current}
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
>
<HelpTooltipContext.Provider value={{ open: isOpen, onClose }}>
{children}
</HelpTooltipContext.Provider>
</HelpPopover>
</>
</PopoverTrigger>
);
};
});

export const HelpTooltipTitle: FC<HTMLAttributes<HTMLHeadingElement>> = ({
children,
Expand Down Expand Up @@ -213,7 +151,7 @@ export const HelpTooltipAction: FC<HelpTooltipActionProps> = ({
onClick,
ariaLabel,
}) => {
const tooltip = useHelpTooltip();
const popover = usePopover();

return (
<button
Expand All @@ -222,7 +160,7 @@ export const HelpTooltipAction: FC<HelpTooltipActionProps> = ({
onClick={(event) => {
event.stopPropagation();
onClick();
tooltip.onClose();
popover.setIsOpen(false);
}}
>
<Icon css={styles.actionIcon} />
Expand Down Expand Up @@ -252,29 +190,13 @@ const getButtonSpacingFromSize = (size?: Size): number => {
const getIconSpacingFromSize = (size?: Size): number => {
switch (size) {
case "small":
return 1.5;
return 12;
case "medium":
default:
return 2;
return 16;
}
};

const classNames = {
popover: (css) => css`
pointer-events: none;
`,

paper: (css, theme) => css`
${theme.typography.body2 as CSSObject}

margin-top: 4px;
width: 304px;
padding: 20px;
color: ${theme.palette.text.secondary};
pointer-events: auto;
`,
} satisfies Record<string, ClassName>;

const styles = {
title: (theme) => ({
marginTop: 0,
Expand Down
22 changes: 13 additions & 9 deletions site/src/components/InfoTooltip/InfoTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { type FC, type ReactNode } from "react";
import {
HelpTooltip,
HelpTooltipContent,
HelpTooltipIcon,
HelpTooltipText,
HelpTooltipTitle,
HelpTooltipTrigger,
} from "components/HelpTooltip/HelpTooltip";
import InfoIcon from "@mui/icons-material/InfoOutlined";
import { Interpolation, Theme, css, useTheme } from "@emotion/react";

interface InfoTooltipProps {
Expand All @@ -22,14 +24,16 @@ export const InfoTooltip: FC<InfoTooltipProps> = ({
const theme = useTheme();

return (
<HelpTooltip
size="small"
icon={InfoIcon}
iconStyles={{ color: theme.experimental.roles[type].outline }}
buttonStyles={styles.button}
>
<HelpTooltipTitle>{title}</HelpTooltipTitle>
<HelpTooltipText>{message}</HelpTooltipText>
<HelpTooltip>
<HelpTooltipTrigger size="small" css={styles.button}>
<HelpTooltipIcon
css={{ color: theme.experimental.roles[type].outline }}
/>
</HelpTooltipTrigger>
<HelpTooltipContent>
<HelpTooltipTitle>{title}</HelpTooltipTitle>
<HelpTooltipText>{message}</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
);
};
Expand Down
Loading