From c0d490f549a0904c81d49f4330a0a57f870b8736 Mon Sep 17 00:00:00 2001 From: Emmanuel Pelletier Date: Wed, 24 Jul 2024 17:19:38 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20new=20Tooltip=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buttons can now easily have tooltip via a new `tooltip` attribute that generates a Tooltip linked to the button --- src/frontend/panda.config.ts | 4 +- .../settings/components/SettingsButton.tsx | 8 +- src/frontend/src/primitives/Button.tsx | 50 +++++++++++-- src/frontend/src/primitives/Dialog.tsx | 6 +- src/frontend/src/primitives/Popover.tsx | 14 ++-- src/frontend/src/primitives/Tooltip.tsx | 74 +++++++++++++++++++ 6 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 src/frontend/src/primitives/Tooltip.tsx diff --git a/src/frontend/panda.config.ts b/src/frontend/panda.config.ts index c92970e8..c62eb20a 100644 --- a/src/frontend/panda.config.ts +++ b/src/frontend/panda.config.ts @@ -47,7 +47,7 @@ const config: Config = { '2xl': '96em', // 1536px }, keyframes: { - popoverSlide: { + slide: { from: { transform: 'var(--origin)', opacity: 0, @@ -57,7 +57,7 @@ const config: Config = { opacity: 1, }, }, - modalFade: { from: { opacity: 0 }, to: { opacity: 1 } }, + fade: { from: { opacity: 0 }, to: { opacity: 1 } }, }, tokens: defineTokens({ /* we take a few things from the panda preset but for now we clear out some stuff. diff --git a/src/frontend/src/features/settings/components/SettingsButton.tsx b/src/frontend/src/features/settings/components/SettingsButton.tsx index d0ddd9e2..ac7c2a47 100644 --- a/src/frontend/src/features/settings/components/SettingsButton.tsx +++ b/src/frontend/src/features/settings/components/SettingsButton.tsx @@ -2,12 +2,16 @@ import { useTranslation } from 'react-i18next' import { RiSettings3Line } from '@remixicon/react' import { Dialog, Button } from '@/primitives' import { SettingsDialog } from './SettingsDialog' - export const SettingsButton = () => { const { t } = useTranslation('settings') return ( - diff --git a/src/frontend/src/primitives/Button.tsx b/src/frontend/src/primitives/Button.tsx index f16001a6..32033cc1 100644 --- a/src/frontend/src/primitives/Button.tsx +++ b/src/frontend/src/primitives/Button.tsx @@ -1,10 +1,13 @@ +import { type ReactNode } from 'react' import { Button as RACButton, type ButtonProps as RACButtonsProps, + TooltipTrigger, Link, LinkProps, } from 'react-aria-components' import { cva, type RecipeVariantProps } from '@/styled-system/css' +import { Tooltip, TooltipArrow } from './Tooltip' const button = cva({ base: { @@ -88,21 +91,52 @@ const button = cva({ }, }) -export type ButtonProps = RecipeVariantProps & RACButtonsProps +type Tooltip = { + tooltip?: string +} +export type ButtonProps = RecipeVariantProps & + RACButtonsProps & + Tooltip -type LinkButtonProps = RecipeVariantProps & LinkProps +type LinkButtonProps = RecipeVariantProps & LinkProps & Tooltip type ButtonOrLinkProps = ButtonProps | LinkButtonProps -export const Button = (props: ButtonOrLinkProps) => { +export const Button = ({ tooltip, ...props }: ButtonOrLinkProps) => { const [variantProps, componentProps] = button.splitVariantProps(props) if ((props as LinkButtonProps).href !== undefined) { - return + return ( + + + + ) } return ( - + + + + ) +} + +const TooltipWrapper = ({ + tooltip, + children, +}: { + tooltip?: string + children: ReactNode +}) => { + return tooltip ? ( + + {children} + + + {tooltip} + + + ) : ( + children ) } diff --git a/src/frontend/src/primitives/Dialog.tsx b/src/frontend/src/primitives/Dialog.tsx index fe7e608d..0a9c5df9 100644 --- a/src/frontend/src/primitives/Dialog.tsx +++ b/src/frontend/src/primitives/Dialog.tsx @@ -24,8 +24,8 @@ const StyledModalOverlay = styled(ModalOverlay, { justifyContent: 'center', alignItems: 'center', zIndex: 1000, - '&[data-entering]': { animation: 'modalFade 200ms' }, - '&[data-exiting]': { animation: 'modalFade 150ms reverse ease-in' }, + '&[data-entering]': { animation: 'fade 200ms' }, + '&[data-exiting]': { animation: 'fade 150ms reverse ease-in' }, }, }) @@ -36,7 +36,7 @@ const StyledModal = styled(Modal, { height: 'full', pointerEvents: 'none', '--origin': 'translateY(32px)', - '&[data-entering]': { animation: 'popoverSlide 300ms' }, + '&[data-entering]': { animation: 'slide 300ms' }, }, }) diff --git a/src/frontend/src/primitives/Popover.tsx b/src/frontend/src/primitives/Popover.tsx index 432a2a61..e50462ab 100644 --- a/src/frontend/src/primitives/Popover.tsx +++ b/src/frontend/src/primitives/Popover.tsx @@ -13,10 +13,10 @@ export const StyledPopover = styled(RACPopover, { base: { minWidth: 'var(--trigger-width)', '&[data-entering]': { - animation: 'popoverSlide 200ms', + animation: 'slide 200ms', }, '&[data-exiting]': { - animation: 'popoverSlide 200ms reverse ease-in', + animation: 'slide 200ms reverse ease-in', }, '&[data-placement="bottom"]': { marginTop: 0.25, @@ -67,11 +67,11 @@ export const Popover = ({ {trigger} - - - - - + + + + + {({ close }) => ( diff --git a/src/frontend/src/primitives/Tooltip.tsx b/src/frontend/src/primitives/Tooltip.tsx new file mode 100644 index 00000000..8c1a659b --- /dev/null +++ b/src/frontend/src/primitives/Tooltip.tsx @@ -0,0 +1,74 @@ +import { OverlayArrow, Tooltip as RACTooltip } from 'react-aria-components' +import { styled } from '@/styled-system/jsx' + +/** + * Styled react aria Tooltip component. + * + * Note that tooltips are directly handled by Buttons via the `tooltip` prop, + * so you should not need to use this component directly. + * + * Style taken from example at https://react-spectrum.adobe.com/react-aria/Tooltip.html + */ +export const Tooltip = styled(RACTooltip, { + base: { + boxShadow: '0 8px 20px rgba(0 0 0 / 0.1)', + borderRadius: '4px', + backgroundColor: 'gray.800', + color: 'gray.100', + forcedColorAdjust: 'none', + outline: 'none', + padding: '2px 8px', + maxWidth: '150px', + transform: 'translate3d(0, 0, 0)', + '&[data-placement=top]': { + marginBottom: '8px', + '--origin': 'translateY(4px)', + }, + '&[data-placement=bottom]': { + marginTop: '8px', + '--origin': 'translateY(-4px)', + }, + '&[data-placement=right]': { + marginLeft: '8px', + '--origin': 'translateX(-4px)', + }, + '&[data-placement=left]': { + marginRight: '8px', + '--origin': 'translateX(4px)', + }, + '& .react-aria-OverlayArrow svg': { + display: 'block', + fill: 'var(--highlight-background)', + }, + '&[data-entering]': { animation: 'slide 200ms' }, + '&[data-exiting]': { animation: 'slide 200ms reverse ease-in' }, + }, +}) + +const StyledOverlayArrow = styled(OverlayArrow, { + base: { + '& svg': { + display: 'block', + fill: 'gray.800', + }, + '&[data-placement=bottom] svg': { + transform: 'rotate(180deg)', + }, + '&[data-placement=right] svg': { + transform: 'rotate(90deg)', + }, + '&[data-placement=left] svg': { + transform: 'rotate(-90deg)', + }, + }, +}) + +export const TooltipArrow = () => { + return ( + + + + + + ) +}