Skip to content

Commit

Permalink
feat: remove close text on modals and add tooltip component (#1146)
Browse files Browse the repository at this point in the history
  • Loading branch information
DasProffi authored Dec 29, 2024
1 parent 0073c71 commit c9946e0
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 11 deletions.
122 changes: 122 additions & 0 deletions lib/components/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { CSSProperties, PropsWithChildren, ReactNode } from 'react'
import { useState } from 'react'
import { tx } from '@twind/core'

type UseHoverStateProps = { closingDelay: number }
const defaultUseHoverStateProps: UseHoverStateProps = {
closingDelay: 200
}

// TODO move this outside of this component and also replace inside the menu component
const useHoverState = (props: Partial<UseHoverStateProps> | undefined = undefined) => {
const { closingDelay } = { ...defaultUseHoverStateProps, ...props }

const [open, setOpen] = useState(false)
const [timer, setTimer] = useState<NodeJS.Timeout>()

const onMouseEnter = () => {
clearTimeout(timer)
setOpen(true)
}

const onMouseLeave = () => {
setTimer(setTimeout(() => {
setOpen(false)
}, closingDelay))
}

return {
open, setOpen, handlers: { onMouseEnter, onMouseLeave }
}
}

type Position = 'top' | 'bottom' | 'left' | 'right'

export type TooltipProps = PropsWithChildren<{
tooltip: string | ReactNode,
/**
* Number of milliseconds until the tooltip appears
*
* defaults to 1000ms
*/
animationDelay?: number,
/**
* Class names of additional styling properties for the tooltip
*/
tooltipClassName?: string,
/**
* Class names of additional styling properties for the container from which the tooltip will be created
*/
containerClassName?: string,
position?: Position,
zIndex?: number,
offset?: number
}>

/**
* A Component for showing a tooltip when hovering over Content
* @param tooltip The tooltip to show can be a text or any ReactNode
* @param children The Content for which the tooltip should be created
* @param animationDelay The delay before the tooltip appears
* @param tooltipClassName Additional ClassNames for the Container of the tooltip
* @param containerClassName Additional ClassNames for the Container holding the content
* @param position The direction of the tooltip relative to the Container
* @param zIndex The z Index of the tooltip (you may require this when stacking modals)
* @param offset The distance to the parent container in pixels
* @constructor
*/
export const Tooltip = ({
tooltip,
children,
animationDelay = 650,
tooltipClassName = '',
containerClassName = '',
position = 'bottom',
zIndex = 10,
offset = 6,
}: TooltipProps) => {
const { open, handlers } = useHoverState()

const positionClasses = {
top: `bottom-full left-1/2 -translate-x-1/2 mb-[${offset}px]`,
bottom: `top-full left-1/2 -translate-x-1/2 mt-[${offset}px]`,
left: `right-full top-1/2 -translate-y-1/2 mr-[${offset}px]`,
right: `left-full top-1/2 -translate-y-1/2 ml-[${offset}px]`
}

const backgroundColor = 'gray-100'
const borderColor = 'gray-600'

const triangleSize = 6
const triangleClasses = {
top: `top-full left-1/2 -translate-x-1/2 border-t-${borderColor} border-l-transparent border-r-transparent`,
bottom: `bottom-full left-1/2 -translate-x-1/2 border-b-${borderColor} border-l-transparent border-r-transparent`,
left: `left-full top-1/2 -translate-y-1/2 border-l-${borderColor} border-t-transparent border-b-transparent`,
right: `right-full top-1/2 -translate-y-1/2 border-r-${borderColor} border-t-transparent border-b-transparent`
}

const triangleStyle: Record<Position, CSSProperties> = {
top: { borderWidth: `${triangleSize}px ${triangleSize}px 0 ${triangleSize}px` },
bottom: { borderWidth: `0 ${triangleSize}px ${triangleSize}px ${triangleSize}px` },
left: { borderWidth: `${triangleSize}px 0 ${triangleSize}px ${triangleSize}px` },
right: { borderWidth: `${triangleSize}px ${triangleSize}px ${triangleSize}px 0` }
}

return (
<div
className={tx(`relative inline-block`, containerClassName)}
{...handlers}
>
{children}
{open && (
<div
className={tx(`opacity-0 absolute z-[${zIndex}] text-black text-xs font-semibold text-[${borderColor}] px-2 py-1 rounded whitespace-nowrap border-2 border-${borderColor}
animate-tooltip-fade-in animation-delay-${animationDelay} shadow-lg bg-${backgroundColor}`, positionClasses[position], tooltipClassName)}
>
{tooltip}
<div className={tx(`absolute w-0 h-0 z-[${zIndex}]`, triangleClasses[position])} style={triangleStyle[position]}/>
</div>
)}
</div>
)
}
10 changes: 6 additions & 4 deletions lib/components/modals/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { tx, tw } from '../../twind'
import type { Languages } from '../../hooks/useLanguage'
import type { PropsForTranslation } from '../../hooks/useTranslation'
import { useTranslation } from '../../hooks/useTranslation'
import { Tooltip } from '../Tooltip'
import { ModalContext } from './ModalRegister'

type ModalHeaderTranslation = {
Expand Down Expand Up @@ -56,10 +57,11 @@ export const ModalHeader = ({
</Span>
)}
{!!onCloseClick && (
<button className={tw('flex flex-row gap-x-2')} onClick={onCloseClick}>
<Span className={tw('mobile:hidden')}>{translation.close}</Span>
<X/>
</button>
<Tooltip tooltip={translation.close}>
<button className={tw('flex flex-row bg-gray-200 hover:bg-gray-300 rounded-md p-1')} onClick={onCloseClick}>
<X/>
</button>
</Tooltip>
)}
</div>
{description ?? (<Span type="description">{descriptionText}</Span>)}
Expand Down
32 changes: 32 additions & 0 deletions lib/stories/other/tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/react'
import { tw } from '@twind/core'
import type { TooltipProps } from '../../../components/Tooltip'
import { Tooltip } from '../../../components/Tooltip'

type TooltipExampleProps = Omit<TooltipProps, 'children' | 'tooltip'> & { tooltipText: string }

const TooltipExample = ({ tooltipText, ...props } : TooltipExampleProps) => {
return (
<Tooltip tooltip={tooltipText} {...props}><span className={tw('bg-hw-primary-400 text-white px-2 py-1 rounded-lg')}>Hover over me</span></Tooltip>
)
}

const meta = {
title: 'Other/Tooltip',
component: TooltipExample,
} satisfies Meta<typeof TooltipExample>

export default meta
type Story = StoryObj<typeof meta>;

export const TooltipExampleStory: Story = {
args: {
tooltipText: 'Tooltip',
animationDelay: 700,
position: 'bottom',
zIndex: 10,
offset: 6,
containerClassName: '',
tooltipClassName: ''
},
}
41 changes: 41 additions & 0 deletions lib/stories/other/tooltip/TooltipStack.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from '@storybook/react'
import { tw } from '@twind/core'
import type { TooltipProps } from '../../../components/Tooltip'
import { Tooltip } from '../../../components/Tooltip'

type TooltipStackExampleProps = Omit<TooltipProps, 'children' | 'tooltip'>

const TooltipStackExample = ({ ...props }: TooltipStackExampleProps) => {
return (
<Tooltip tooltip={(
<Tooltip zIndex={11} tooltip={(
<span>Try to hover <Tooltip tooltip="Great right?" zIndex={12}>
<span className={tw('font-bold underline')}>here</span>
</Tooltip></span>
)}>This is a Text on which you can hover to show
another Tooltip
</Tooltip>
)} {...props}>
<span className={tw('bg-hw-primary-400 text-white px-2 py-1 rounded-lg')}>Hover over me</span>
</Tooltip>
)
}

const meta = {
title: 'Other/Tooltip',
component: TooltipStackExample,
} satisfies Meta<typeof TooltipStackExample>

export default meta
type Story = StoryObj<typeof meta>;

export const TooltipStackExampleStory: Story = {
args: {
animationDelay: 700,
position: 'right',
zIndex: 10,
offset: 6,
containerClassName: '',
tooltipClassName: ''
},
}
23 changes: 16 additions & 7 deletions lib/twind/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,39 +148,45 @@ export const config = defineConfig({
},
screens: screenSizes,
animation: {
'fade': 'fadeOut 3s ease-in-out',
'fade': 'fade-out 3s ease-in-out',
'wave-big-left-up': 'bigLeftUp 1.7s ease-in 0s infinite normal',
'wave-big-right-down': 'bigRightDown 1.7s ease-in 0s infinite reverse',
'wave-small-left-up': 'smallLeftUp 1.7s ease-in 0s infinite normal',
'wave-small-right-down': 'smallRightDown 1.7s ease-in 0s infinite reverse',
'tooltip-fade-in': 'fade-in 0.2s ease-in-out forwards',
'tooltip-fade-out': 'fade-in 0.2s ease-in-out forwards',
},
keyframes: {
fadeOut: {
'fade-in': {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
'fade-out': {
'0%': { opacity: '100%' },
'100%': { opacity: '0%' },
},
bigLeftUp: {
'bigLeftUp': {
'0%': { strokeDashoffset: '1000' },
'25%': { strokeDashoffset: '1000' },
'50%': { strokeDashoffset: '0' },
'75%': { strokeDashoffset: '0' },
'100%': { strokeDashoffset: '0' },
},
bigRightDown: {
'bigRightDown': {
'0%': { strokeDashoffset: '0' },
'25%': { strokeDashoffset: '0' },
'50%': { strokeDashoffset: '0' },
'75%': { strokeDashoffset: '-1000' },
'100%': { strokeDashoffset: '-1000' },
},
smallLeftUp: {
'smallLeftUp': {
'0%': { strokeDashoffset: '1000' },
'25%': { strokeDashoffset: '1000' },
'50%': { strokeDashoffset: '1000' },
'75%': { strokeDashoffset: '0' },
'100%': { strokeDashoffset: '0' },
},
smallRightDown: {
'smallRightDown': {
'0%': { strokeDashoffset: '0' },
'25%': { strokeDashoffset: '0' },
'50%': { strokeDashoffset: '-1000' },
Expand All @@ -196,7 +202,10 @@ export const config = defineConfig({
}
}
},
presets: [presetAutoprefix(), presetTailwind(), presetTailwindForms(), presetTypography()]
presets: [presetAutoprefix(), presetTailwind(), presetTailwindForms(), presetTypography()],
rules: [
['animation-delay-', ({ $$ }) => ({ animationDelay: `${$$}ms` })]
]
}) as unknown as TwindConfig // TODO: twind is being very dumb right here, not my fault

export default config

0 comments on commit c9946e0

Please sign in to comment.