Skip to content

Commit

Permalink
Update Tooltip with Radix and Tailwind (#709)
Browse files Browse the repository at this point in the history
* Install @radix-ui/react-tooltip

* Use SidebarButton directly

* Make attribute value text medium font weight

* Update Tooltip to Radix
  • Loading branch information
kmcginnes authored Dec 10, 2024
1 parent def1496 commit f10700e
Show file tree
Hide file tree
Showing 18 changed files with 174 additions and 219 deletions.
1 change: 1 addition & 0 deletions packages/graph-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@react-aria/button": "3.10.1",
"@react-aria/checkbox": "3.14.8",
"@react-aria/combobox": "3.10.5",
Expand Down
17 changes: 6 additions & 11 deletions packages/graph-explorer/src/components/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { cn } from "@/utils";
import type { ComponentPropsWithoutRef, ForwardedRef, ReactNode } from "react";
import { forwardRef } from "react";
import type { TooltipProps } from "./Tooltip";
import Tooltip from "./Tooltip/Tooltip";
import { cva, type VariantProps } from "cva";
import { Tooltip, TooltipContent } from "@/components";
import { TooltipTrigger } from "@radix-ui/react-tooltip";

const iconButtonVariants = cva({
base: "focus-visible:ring-ring inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 disabled:saturate-0 [&_svg]:pointer-events-none [&_svg]:shrink-0",
Expand Down Expand Up @@ -55,15 +55,13 @@ export interface IconButtonProps
extends Omit<ComponentPropsWithoutRef<"button">, "color">,
VariantProps<typeof iconButtonVariants> {
icon: ReactNode;
tooltipText?: TooltipProps["text"];
tooltipPlacement?: TooltipProps["placement"];
tooltipText?: React.ReactNode;
}

export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
(
{
tooltipText,
tooltipPlacement,
variant,
size,
color,
Expand All @@ -86,12 +84,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(

if (tooltipText) {
return (
<Tooltip
text={tooltipText}
placement={tooltipPlacement}
delayEnter={400}
>
{component}
<Tooltip>
<TooltipTrigger asChild>{component}</TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent>
</Tooltip>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ export function ExternalExportControl<T extends Record<string, unknown>>({
return (
<Popover>
<PopoverTrigger asChild>
<IconButton variant="text" icon={<TrayArrowIcon />} />
<IconButton
variant="text"
icon={<TrayArrowIcon />}
tooltipText="Export table"
/>
</PopoverTrigger>
<PopoverContent side="right" className="w-72">
<ExportOptionsModal
Expand Down
24 changes: 6 additions & 18 deletions packages/graph-explorer/src/components/Tooltip/InfoTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
import { PropsWithChildren } from "react";
import { InfoIcon } from "@/components/icons";
import Tooltip from "./Tooltip";
import { useTheme } from "@/core";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components";

export default function InfoTooltip({ children }: PropsWithChildren) {
const theme = useTheme();

return (
<Tooltip text={<div style={{ maxWidth: 300 }}>{children}</div>}>
<div
style={{
display: "flex",
gap: 2,
alignItems: "center",
justifyItems: "center",
}}
>
<InfoIcon
color={theme.theme.palette.text.secondary}
style={{ width: 22, height: 22 }}
/>
</div>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="text-text-secondary size-6" />
</TooltipTrigger>
<TooltipContent>{children}</TooltipContent>
</Tooltip>
);
}
13 changes: 0 additions & 13 deletions packages/graph-explorer/src/components/Tooltip/Tooltip.styles.ts

This file was deleted.

118 changes: 22 additions & 96 deletions packages/graph-explorer/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,103 +1,29 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/utils";
import { AnimatePresence, motion } from "framer-motion";
import type { PropsWithChildren, ReactNode } from "react";
import { cloneElement, useEffect } from "react";
import { Arrow, useHover, useLayer } from "react-laag";
import type { PlacementType } from "react-laag/dist/PlacementType";
import { useWithTheme } from "@/core";
import usePrevious from "@/hooks/usePrevious";
import { tooltipStyles } from "./Tooltip.styles";

function isReactText(children: ReactNode) {
return ["string", "number"].includes(typeof children);
}
const TooltipProvider = TooltipPrimitive.Provider;

export type TooltipProps = {
text: ReactNode;
placement?: PlacementType;
delayEnter?: number;
delayLeave?: number;
triggerOffset?: number;
className?: string;
disabled?: boolean;
onHoverChange?: (isOver: boolean) => void;
};
const Tooltip = TooltipPrimitive.Root;

export const Tooltip = ({
children,
text,
placement = "bottom-center",
delayEnter = 100,
delayLeave = 300,
triggerOffset = 8,
className,
disabled,
onHoverChange,
}: PropsWithChildren<TooltipProps>) => {
const [isOver, hoverProps] = useHover({ delayEnter, delayLeave });
const [isOverTooltip, hoverTooltipProps] = useHover({
delayEnter,
delayLeave,
});
const { triggerProps, layerProps, arrowProps, renderLayer } = useLayer({
isOpen: !disabled && (isOver || isOverTooltip),
auto: true,
placement,
triggerOffset,
});
const TooltipTrigger = TooltipPrimitive.Trigger;

const prevIsOver = usePrevious(isOver);

useEffect(() => {
if (prevIsOver !== isOver) onHoverChange?.(isOver);
}, [isOver, onHoverChange, prevIsOver]);
// when children equals text (string | number), we need to wrap it in an
// extra span-element in order to attach props
let trigger;
if (isReactText(children)) {
trigger = (
<div className="tooltip-text-wrapper" {...triggerProps} {...hoverProps}>
{children}
</div>
);
} else {
// In case of an react-element, we need to clone it in order to attach our own props
trigger = cloneElement(children as any, {
...triggerProps,
...hoverProps,
});
}

const stylesWithTheme = useWithTheme();

return (
<>
{trigger}
{renderLayer(
<AnimatePresence>
{!disabled && (isOver || isOverTooltip) && (
<motion.div
className={cn(stylesWithTheme(tooltipStyles), className)}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.1 }}
{...layerProps}
style={{ pointerEvents: "none", ...layerProps.style }}
>
<span {...hoverTooltipProps}>{text}</span>
<Arrow
{...arrowProps}
backgroundColor="rgb(78, 78, 78)"
borderColor="transparent"
size={6}
/>
</motion.div>
)}
</AnimatePresence>
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"bg-text-primary text-background-default animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-tooltip max-w-96 overflow-hidden rounded-md px-3 py-1.5 text-sm",
className
)}
</>
);
};
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;

export default Tooltip;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
3 changes: 1 addition & 2 deletions packages/graph-explorer/src/components/Tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { default as Tooltip } from "./Tooltip";
export * from "./Tooltip";
export { default as InfoTooltip } from "./InfoTooltip";
export type { TooltipProps } from "./Tooltip";
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { Tooltip } from "../../Tooltip";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components";
import { cn } from "@/utils";

const SidebarButton = React.forwardRef<
Expand All @@ -13,22 +13,25 @@ const SidebarButton = React.forwardRef<
>(({ icon, title, badge = false, className, children, ...props }, ref) => {
return (
<Badge value={badge}>
<Tooltip text={title} placement="left-center" delayEnter={300}>
<TogglePrimitive.Root
ref={ref}
className={cn(
"text-brand-900 data-[state=on]:bg-brand-500 active:bg-brand-300 inline-flex size-10 items-center justify-center rounded-md bg-transparent p-2 ring-0 transition-colors duration-100 focus:shadow-none active:text-white disabled:pointer-events-none disabled:opacity-50 data-[state=on]:text-white [&_svg]:size-6",
"hover:bg-brand-200/50 hover:text-primary-dark hover:data-[state=on]:bg-brand-400",
"dark:text-brand-300 dark:hover:data-[state=on]:bg-brand-500 dark:data-[state=on]:bg-brand-400 dark:hover:bg-gray-800 dark:data-[state=on]:text-white",
"focus-visible:ring-brand-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 active:ring-0",
className
)}
{...props}
>
{icon}
{children}
<span className="sr-only">{title}</span>
</TogglePrimitive.Root>
<Tooltip>
<TooltipTrigger>
<TogglePrimitive.Root
ref={ref}
className={cn(
"text-brand-900 data-[state=on]:bg-brand-500 active:bg-brand-300 inline-flex size-10 items-center justify-center rounded-md bg-transparent p-2 ring-0 transition-colors duration-100 focus:shadow-none active:text-white disabled:pointer-events-none disabled:opacity-50 data-[state=on]:text-white [&_svg]:size-6",
"hover:bg-brand-200/50 hover:text-primary-dark hover:data-[state=on]:bg-brand-400",
"dark:text-brand-300 dark:hover:data-[state=on]:bg-brand-500 dark:data-[state=on]:bg-brand-400 dark:hover:bg-gray-800 dark:data-[state=on]:text-white",
"focus-visible:ring-brand-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 active:ring-0",
className
)}
{...props}
>
{icon}
{children}
<span className="sr-only">{title}</span>
</TogglePrimitive.Root>
</TooltipTrigger>
<TooltipContent side="left">{title}</TooltipContent>
</Tooltip>
</Badge>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import type { PropsWithChildren, ReactElement } from "react";
import { useMemo } from "react";
import getChildOfType from "@/utils/getChildOfType";
import getChildrenOfType from "@/utils/getChildrenOfType";
import { SidebarButton } from "@/components/Workspace/components/SidebarButton";
import WorkspaceSideBarContent from "./WorkspaceSideBarContent";

interface WorkspaceSideBarComposition {
Button: typeof SidebarButton;
Content: typeof WorkspaceSideBarContent;
}

Expand Down Expand Up @@ -52,7 +50,6 @@ const WorkspaceSideBar = ({

WorkspaceSideBar.displayName = "WorkspaceSideBar";

WorkspaceSideBar.Button = SidebarButton;
WorkspaceSideBar.Content = WorkspaceSideBarContent;

export default WorkspaceSideBar as ((
Expand Down
1 change: 1 addition & 0 deletions packages/graph-explorer/src/components/Workspace/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export { default as WorkspaceTopBarTitle } from "./components/WorkspaceTopBarTit
export type { WorkspaceTopBarTitleProps } from "./components/WorkspaceTopBarTitle";
export { default as WorkspacesContent } from "./components/WorkspacesContent";
export { default as WorkspaceSideBar } from "./components/WorkspaceSideBar";
export * from "./components/SidebarButton";
export { default as WorkspaceSideBarContent } from "./components/WorkspaceSideBarContent";
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { emotionTransform, MantineEmotionProvider } from "@mantine/emotion";
import { ExpandNodeProvider } from "@/hooks/useExpandNode";
import { ErrorBoundary } from "react-error-boundary";
import AppErrorPage from "@/core/AppErrorPage";
import { TooltipProvider } from "@/components";

export type ConnectedProviderProps = {
config?: RawConfiguration;
Expand Down Expand Up @@ -38,19 +39,21 @@ const ConnectedProvider = (
return (
<ErrorBoundary FallbackComponent={AppErrorPage}>
<QueryClientProvider client={queryClient}>
<MantineProvider stylesTransform={emotionTransform}>
<MantineEmotionProvider>
<ThemeProvider>
<NotificationProvider component={Toast}>
<StateProvider>
<AppStatusLoader config={config}>
<ExpandNodeProvider>{children}</ExpandNodeProvider>
</AppStatusLoader>
</StateProvider>
</NotificationProvider>
</ThemeProvider>
</MantineEmotionProvider>
</MantineProvider>
<TooltipProvider delayDuration={200}>
<MantineProvider stylesTransform={emotionTransform}>
<MantineEmotionProvider>
<ThemeProvider>
<NotificationProvider component={Toast}>
<StateProvider>
<AppStatusLoader config={config}>
<ExpandNodeProvider>{children}</ExpandNodeProvider>
</AppStatusLoader>
</StateProvider>
</NotificationProvider>
</ThemeProvider>
</MantineEmotionProvider>
</MantineProvider>
</TooltipProvider>
</QueryClientProvider>
</ErrorBoundary>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function EntityAttribute({
<div className="text-text-secondary text-balance break-words text-sm">
{attribute.displayLabel}
</div>
<div className="text-text-primary text-balance break-words">
<div className="text-text-primary text-balance break-words font-medium">
{attribute.displayValue}
</div>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ export default function GraphViewer({
/>
<IconButton
tooltipText="Re-run Layout"
tooltipPlacement="bottom-center"
icon={<ResetIcon />}
variant="text"
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ export default function SingleNodeStyling({
</>
}
tooltipText="Upload New Icon"
tooltipPlacement="bottom-end"
onClick={props.onClick}
/>
)}
Expand Down
Loading

0 comments on commit f10700e

Please sign in to comment.