✨(frontend) new Tooltip component
buttons can now easily have tooltip via a new `tooltip` attribute that generates a Tooltip linked to the button
This commit is contained in:
@@ -47,7 +47,7 @@ const config: Config = {
|
|||||||
'2xl': '96em', // 1536px
|
'2xl': '96em', // 1536px
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
popoverSlide: {
|
slide: {
|
||||||
from: {
|
from: {
|
||||||
transform: 'var(--origin)',
|
transform: 'var(--origin)',
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@@ -57,7 +57,7 @@ const config: Config = {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modalFade: { from: { opacity: 0 }, to: { opacity: 1 } },
|
fade: { from: { opacity: 0 }, to: { opacity: 1 } },
|
||||||
},
|
},
|
||||||
tokens: defineTokens({
|
tokens: defineTokens({
|
||||||
/* we take a few things from the panda preset but for now we clear out some stuff.
|
/* we take a few things from the panda preset but for now we clear out some stuff.
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { RiSettings3Line } from '@remixicon/react'
|
import { RiSettings3Line } from '@remixicon/react'
|
||||||
import { Dialog, Button } from '@/primitives'
|
import { Dialog, Button } from '@/primitives'
|
||||||
import { SettingsDialog } from './SettingsDialog'
|
import { SettingsDialog } from './SettingsDialog'
|
||||||
|
|
||||||
export const SettingsButton = () => {
|
export const SettingsButton = () => {
|
||||||
const { t } = useTranslation('settings')
|
const { t } = useTranslation('settings')
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<Button square invisible aria-label={t('settingsButtonLabel')}>
|
<Button
|
||||||
|
square
|
||||||
|
invisible
|
||||||
|
aria-label={t('settingsButtonLabel')}
|
||||||
|
tooltip={t('settingsButtonLabel')}
|
||||||
|
>
|
||||||
<RiSettings3Line />
|
<RiSettings3Line />
|
||||||
</Button>
|
</Button>
|
||||||
<SettingsDialog />
|
<SettingsDialog />
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import { type ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
Button as RACButton,
|
Button as RACButton,
|
||||||
type ButtonProps as RACButtonsProps,
|
type ButtonProps as RACButtonsProps,
|
||||||
|
TooltipTrigger,
|
||||||
Link,
|
Link,
|
||||||
LinkProps,
|
LinkProps,
|
||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
import { cva, type RecipeVariantProps } from '@/styled-system/css'
|
import { cva, type RecipeVariantProps } from '@/styled-system/css'
|
||||||
|
import { Tooltip, TooltipArrow } from './Tooltip'
|
||||||
|
|
||||||
const button = cva({
|
const button = cva({
|
||||||
base: {
|
base: {
|
||||||
@@ -88,21 +91,52 @@ const button = cva({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export type ButtonProps = RecipeVariantProps<typeof button> & RACButtonsProps
|
type Tooltip = {
|
||||||
|
tooltip?: string
|
||||||
|
}
|
||||||
|
export type ButtonProps = RecipeVariantProps<typeof button> &
|
||||||
|
RACButtonsProps &
|
||||||
|
Tooltip
|
||||||
|
|
||||||
type LinkButtonProps = RecipeVariantProps<typeof button> & LinkProps
|
type LinkButtonProps = RecipeVariantProps<typeof button> & LinkProps & Tooltip
|
||||||
|
|
||||||
type ButtonOrLinkProps = ButtonProps | LinkButtonProps
|
type ButtonOrLinkProps = ButtonProps | LinkButtonProps
|
||||||
|
|
||||||
export const Button = (props: ButtonOrLinkProps) => {
|
export const Button = ({ tooltip, ...props }: ButtonOrLinkProps) => {
|
||||||
const [variantProps, componentProps] = button.splitVariantProps(props)
|
const [variantProps, componentProps] = button.splitVariantProps(props)
|
||||||
if ((props as LinkButtonProps).href !== undefined) {
|
if ((props as LinkButtonProps).href !== undefined) {
|
||||||
return <Link className={button(variantProps)} {...componentProps} />
|
return (
|
||||||
|
<TooltipWrapper tooltip={tooltip}>
|
||||||
|
<Link className={button(variantProps)} {...componentProps} />
|
||||||
|
</TooltipWrapper>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<RACButton
|
<TooltipWrapper tooltip={tooltip}>
|
||||||
className={button(variantProps)}
|
<RACButton
|
||||||
{...(componentProps as RACButtonsProps)}
|
className={button(variantProps)}
|
||||||
/>
|
{...(componentProps as RACButtonsProps)}
|
||||||
|
/>
|
||||||
|
</TooltipWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TooltipWrapper = ({
|
||||||
|
tooltip,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
tooltip?: string
|
||||||
|
children: ReactNode
|
||||||
|
}) => {
|
||||||
|
return tooltip ? (
|
||||||
|
<TooltipTrigger delay={300}>
|
||||||
|
{children}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipArrow />
|
||||||
|
{tooltip}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ const StyledModalOverlay = styled(ModalOverlay, {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
'&[data-entering]': { animation: 'modalFade 200ms' },
|
'&[data-entering]': { animation: 'fade 200ms' },
|
||||||
'&[data-exiting]': { animation: 'modalFade 150ms reverse ease-in' },
|
'&[data-exiting]': { animation: 'fade 150ms reverse ease-in' },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ const StyledModal = styled(Modal, {
|
|||||||
height: 'full',
|
height: 'full',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
'--origin': 'translateY(32px)',
|
'--origin': 'translateY(32px)',
|
||||||
'&[data-entering]': { animation: 'popoverSlide 300ms' },
|
'&[data-entering]': { animation: 'slide 300ms' },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ export const StyledPopover = styled(RACPopover, {
|
|||||||
base: {
|
base: {
|
||||||
minWidth: 'var(--trigger-width)',
|
minWidth: 'var(--trigger-width)',
|
||||||
'&[data-entering]': {
|
'&[data-entering]': {
|
||||||
animation: 'popoverSlide 200ms',
|
animation: 'slide 200ms',
|
||||||
},
|
},
|
||||||
'&[data-exiting]': {
|
'&[data-exiting]': {
|
||||||
animation: 'popoverSlide 200ms reverse ease-in',
|
animation: 'slide 200ms reverse ease-in',
|
||||||
},
|
},
|
||||||
'&[data-placement="bottom"]': {
|
'&[data-placement="bottom"]': {
|
||||||
marginTop: 0.25,
|
marginTop: 0.25,
|
||||||
@@ -67,11 +67,11 @@ export const Popover = ({
|
|||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
{trigger}
|
{trigger}
|
||||||
<StyledPopover>
|
<StyledPopover>
|
||||||
<StyledOverlayArrow>
|
<StyledOverlayArrow>
|
||||||
<svg width={12} height={12} viewBox="0 0 12 12">
|
<svg width={12} height={12} viewBox="0 0 12 12">
|
||||||
<path d="M0 0 L6 6 L12 0" />
|
<path d="M0 0 L6 6 L12 0" />
|
||||||
</svg>
|
</svg>
|
||||||
</StyledOverlayArrow>
|
</StyledOverlayArrow>
|
||||||
<Dialog {...dialogProps}>
|
<Dialog {...dialogProps}>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
<Box size="sm" type="popover">
|
<Box size="sm" type="popover">
|
||||||
|
|||||||
74
src/frontend/src/primitives/Tooltip.tsx
Normal file
74
src/frontend/src/primitives/Tooltip.tsx
Normal file
@@ -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 (
|
||||||
|
<StyledOverlayArrow>
|
||||||
|
<svg width={8} height={8} viewBox="0 0 8 8">
|
||||||
|
<path d="M0 0 L4 4 L8 0" />
|
||||||
|
</svg>
|
||||||
|
</StyledOverlayArrow>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user