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

feat(components): add Tooltip and TooltipTrigger #1143

Merged
merged 12 commits into from
Jan 22, 2024
6 changes: 6 additions & 0 deletions .changeset/nasty-panthers-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@launchpad-ui/components": patch
"@launchpad-ui/tokens": patch
---

Add `Tooltip` and `TooltipTrigger`
1 change: 1 addition & 0 deletions apps/remix/app/data.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export async function getComponents() {
{ to: 'rac/button', name: 'RAC Button', role: 'button' },
{ to: 'rac/link-button', name: 'RAC LinkButton', role: 'link' },
{ to: 'rac/progress-bar', name: 'RAC ProgressBar', role: 'progressbar' },
{ to: 'rac/tooltip', name: 'RAC Tooltip', role: 'tooltip' },
// plop end components
];
}
10 changes: 10 additions & 0 deletions apps/remix/app/routes/rac.tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Button, Tooltip, TooltipTrigger } from '@launchpad-ui/components';

export default function Index() {
return (
<TooltipTrigger>
<Button>Trigger</Button>
<Tooltip isOpen>Message</Tooltip>
</TooltipTrigger>
);
}
19 changes: 19 additions & 0 deletions packages/components/__tests__/Tooltip.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { it, expect, describe } from 'vitest';

import { render, screen, userEvent } from '../../../test/utils';
import { Button, Tooltip, TooltipTrigger } from '../src';

describe('Tooltip', () => {
it('renders', async () => {
const user = userEvent.setup();
render(
<TooltipTrigger>
<Button>Trigger</Button>
<Tooltip>Message</Tooltip>
</TooltipTrigger>
);
await user.hover(document.body);
await user.hover(screen.getByRole('button'));
expect(await screen.findByRole('tooltip')).toBeVisible();
});
});
16 changes: 7 additions & 9 deletions packages/components/src/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,14 @@ const button = cva(styles.base, {
type ButtonVariants = VariantProps<typeof button>;
type ButtonProps = AriaButtonProps & ButtonVariants;

const Button = forwardRef(
(
{ size = 'medium', variant = 'default', className, ...props }: ButtonProps,
ref: ForwardedRef<HTMLButtonElement>
) => {
return <AriaButton {...props} ref={ref} className={button({ size, variant, className })} />;
}
);
const _Button = (
{ size = 'medium', variant = 'default', className, ...props }: ButtonProps,
ref: ForwardedRef<HTMLButtonElement>
) => {
return <AriaButton {...props} ref={ref} className={button({ size, variant, className })} />;
};

Button.displayName = 'Button';
const Button = forwardRef(_Button);

export { Button, button };
export type { ButtonProps, ButtonVariants };
24 changes: 11 additions & 13 deletions packages/components/src/ButtonGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,18 @@ type ButtonGroupProps = ComponentPropsWithRef<'div'> &
isDisabled?: boolean;
};

const ButtonGroup = forwardRef(
(
{ children, className, spacing = 'basic', isDisabled, ...props }: ButtonGroupProps,
ref: ForwardedRef<HTMLDivElement>
) => {
return (
<div {...props} ref={ref} className={buttonGroup({ spacing, className })}>
<ButtonContext.Provider value={{ isDisabled }}>{children}</ButtonContext.Provider>
</div>
);
}
);
const _ButtonGroup = (
{ children, className, spacing = 'basic', isDisabled, ...props }: ButtonGroupProps,
ref: ForwardedRef<HTMLDivElement>
) => {
return (
<div {...props} ref={ref} className={buttonGroup({ spacing, className })}>
<ButtonContext.Provider value={{ isDisabled }}>{children}</ButtonContext.Provider>
</div>
);
};

ButtonGroup.displayName = 'ButtonGroup';
const ButtonGroup = forwardRef(_ButtonGroup);

export { ButtonGroup };
export type { ButtonGroupProps };
32 changes: 15 additions & 17 deletions packages/components/src/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,22 @@ type IconButtonProps = Omit<AriaButtonProps, 'children'> &
variant?: Extract<ButtonVariants['variant'], 'default' | 'primary' | 'destructive' | 'minimal'>;
};

const IconButton = forwardRef(
(
{ size = 'medium', variant = 'default', className, icon, ...props }: IconButtonProps,
ref: ForwardedRef<HTMLButtonElement>
) => {
return (
<AriaButton
{...props}
ref={ref}
className={cx(button({ size, variant }), iconButton({ size, className }))}
>
<Icon name={icon} size="small" aria-hidden />
</AriaButton>
);
}
);
const _IconButton = (
{ size = 'medium', variant = 'default', className, icon, ...props }: IconButtonProps,
ref: ForwardedRef<HTMLButtonElement>
) => {
return (
<AriaButton
{...props}
ref={ref}
className={cx(button({ size, variant }), iconButton({ size, className }))}
>
<Icon name={icon} size="small" aria-hidden />
</AriaButton>
);
};

IconButton.displayName = 'IconButton';
const IconButton = forwardRef(_IconButton);

export { IconButton };
export type { IconButtonProps };
78 changes: 38 additions & 40 deletions packages/components/src/LinkButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,46 @@ import { button } from './Button';

type LinkButtonProps = LinkProps & RouterLinkProps & ButtonVariants;

const LinkButton = forwardRef(
(
{
size = 'medium',
variant = 'default',
className,
to,
replace,
state,
target,
...props
}: LinkButtonProps,
ref: ForwardedRef<HTMLAnchorElement>
) => {
const href = useHref(to);
const handleClick = useLinkClickHandler(to, {
replace,
state,
target,
});
const _LinkButton = (
{
size = 'medium',
variant = 'default',
className,
to,
replace,
state,
target,
...props
}: LinkButtonProps,
ref: ForwardedRef<HTMLAnchorElement>
) => {
const href = useHref(to);
const handleClick = useLinkClickHandler(to, {
replace,
state,
target,
});

return (
<Link
{...props}
ref={ref}
className={button({ size, variant, className })}
href={href}
onPress={(event) => {
props.onPress?.(event);
// https://reactrouter.com/en/main/hooks/use-link-click-handler
handleClick({
...event,
button: 0, // https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/dom.ts#L41
preventDefault: () => undefined,
} as unknown as MouseEvent<HTMLAnchorElement>);
}}
/>
);
}
);
return (
<Link
{...props}
ref={ref}
className={button({ size, variant, className })}
href={href}
onPress={(event) => {
props.onPress?.(event);
// https://reactrouter.com/en/main/hooks/use-link-click-handler
handleClick({
...event,
button: 0, // https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/dom.ts#L41
preventDefault: () => undefined,
} as unknown as MouseEvent<HTMLAnchorElement>);
}}
/>
);
};

LinkButton.displayName = 'LinkButton';
const LinkButton = forwardRef(_LinkButton);

export { LinkButton };
export type { LinkButtonProps };
82 changes: 40 additions & 42 deletions packages/components/src/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,51 +23,49 @@ const progressBar = cva(styles.base, {

type ProgressBarProps = AriaProgressBarProps & VariantProps<typeof progressBar>;

const ProgressBar = forwardRef(
(
{ size = 'small', className, ...props }: ProgressBarProps,
ref: ForwardedRef<HTMLDivElement>
) => {
const center = 16;
const strokeWidth = 4;
const r = 16 - strokeWidth;
const c = 2 * r * Math.PI;
const _ProgressBar = (
{ size = 'small', className, ...props }: ProgressBarProps,
ref: ForwardedRef<HTMLDivElement>
) => {
const center = 16;
const strokeWidth = 4;
const r = 16 - strokeWidth;
const c = 2 * r * Math.PI;

return (
<AriaProgressBar {...props} ref={ref} className={progressBar({ size, className })}>
{({ percentage }) => (
<svg
width={64}
height={64}
viewBox="0 0 32 32"
fill="none"
return (
<AriaProgressBar {...props} ref={ref} className={progressBar({ size, className })}>
{({ percentage }) => (
<svg
width={64}
height={64}
viewBox="0 0 32 32"
fill="none"
strokeWidth={strokeWidth}
className={cx(props.isIndeterminate && styles.indeterminate)}
>
<circle
cx={center}
cy={center}
r={r}
strokeWidth={strokeWidth}
className={cx(props.isIndeterminate && styles.indeterminate)}
>
<circle
cx={center}
cy={center}
r={r}
strokeWidth={strokeWidth}
className={styles.outerCircle}
/>
<circle
cx={center}
cy={center}
r={r}
strokeDasharray={`${c} ${c}`}
strokeDashoffset={c - (props.isIndeterminate ? 0.34 : percentage! / 100) * c}
transform="rotate(-90 16 16)"
className={styles.innerCircle}
/>
</svg>
)}
</AriaProgressBar>
);
}
);
className={styles.outerCircle}
/>
<circle
cx={center}
cy={center}
r={r}
strokeDasharray={`${c} ${c}`}
strokeDashoffset={c - (props.isIndeterminate ? 0.34 : percentage! / 100) * c}
transform="rotate(-90 16 16)"
className={styles.innerCircle}
/>
</svg>
)}
</AriaProgressBar>
);
};

ProgressBar.displayName = 'ProgressBar';
const ProgressBar = forwardRef(_ProgressBar);

export { ProgressBar };
export type { ProgressBarProps };
38 changes: 38 additions & 0 deletions packages/components/src/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ForwardedRef } from 'react';
import type {
TooltipProps as AriaTooltipProps,
TooltipTriggerComponentProps,
} from 'react-aria-components';

import { cx } from 'class-variance-authority';
import { forwardRef } from 'react';
import {
Tooltip as AriaTooltip,
TooltipTrigger as AriaTooltipTrigger,
} from 'react-aria-components';

import styles from './styles/Tooltip.module.css';

type TooltipProps = Omit<AriaTooltipProps, 'offset' | 'crossOffset'>;
type TooltipTriggerProps = Omit<TooltipTriggerComponentProps, 'delay' | 'closeDelay'>;

const _Tooltip = ({ className, ...props }: TooltipProps, ref: ForwardedRef<HTMLDivElement>) => {
return (
<AriaTooltip
{...props}
offset={0}
crossOffset={0}
ref={ref}
className={cx(styles.tooltip, className)}
/>
);
};

const Tooltip = forwardRef(_Tooltip);

const TooltipTrigger = (props: TooltipTriggerProps) => {
return <AriaTooltipTrigger {...props} delay={500} closeDelay={0} />;
};

export { Tooltip, TooltipTrigger };
export type { TooltipProps, TooltipTriggerProps };
2 changes: 2 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ export type { ButtonGroupProps } from './ButtonGroup';
export type { IconButtonProps } from './IconButton';
export type { LinkButtonProps } from './LinkButton';
export type { ProgressBarProps } from './ProgressBar';
export type { TooltipProps, TooltipTriggerProps } from './Tooltip';

export { Button } from './Button';
export { ButtonGroup } from './ButtonGroup';
export { IconButton } from './IconButton';
export { LinkButton } from './LinkButton';
export { ProgressBar } from './ProgressBar';
export { Tooltip, TooltipTrigger } from './Tooltip';
Loading
Loading