Merge pull request #17736 from overleaf/ii-bs5-tooltip

[web] Create Btoostrap 5 tooltips

GitOrigin-RevId: 28c7c389bda74765482049750fc0ae8a5995968e
This commit is contained in:
M Fahru 2024-04-15 06:34:32 -07:00 committed by Copybot
parent a0da44358f
commit 9f38436f10
9 changed files with 210 additions and 7 deletions

View file

@ -1,5 +1,4 @@
import { useState } from 'react' import { useState } from 'react'
import Tooltip from '../../../../../../shared/components/tooltip'
import PrimaryButton from './primary-button' import PrimaryButton from './primary-button'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
@ -15,6 +14,7 @@ import { UserEmailData } from '../../../../../../../../types/user-email'
import { UseAsyncReturnType } from '../../../../../../shared/hooks/use-async' import { UseAsyncReturnType } from '../../../../../../shared/hooks/use-async'
import { ssoAvailableForInstitution } from '../../../../utils/sso' import { ssoAvailableForInstitution } from '../../../../utils/sso'
import ConfirmationModal from './confirmation-modal' import ConfirmationModal from './confirmation-modal'
import TooltipWrapper from '@/features/ui/components/bootstrap-5/wrappers/tooltip-wrapper'
const getDescription = ( const getDescription = (
t: (s: string) => string, t: (s: string) => string,
@ -86,7 +86,7 @@ function MakePrimary({ userEmailData, makePrimaryAsync }: MakePrimaryProps) {
{t('processing_uppercase')}… {t('processing_uppercase')}…
</PrimaryButton> </PrimaryButton>
) : ( ) : (
<Tooltip <TooltipWrapper
id={`make-primary-${userEmailData.email}`} id={`make-primary-${userEmailData.email}`}
description={getDescription(t, state, userEmailData)} description={getDescription(t, state, userEmailData)}
> >
@ -102,7 +102,7 @@ function MakePrimary({ userEmailData, makePrimaryAsync }: MakePrimaryProps) {
{t('make_primary')} {t('make_primary')}
</PrimaryButton> </PrimaryButton>
</span> </span>
</Tooltip> </TooltipWrapper>
)} )}
<ConfirmationModal <ConfirmationModal
email={userEmailData.email} email={userEmailData.email}

View file

@ -1,13 +1,13 @@
import Icon from '../../../../../shared/components/icon' import Icon from '../../../../../shared/components/icon'
import Tooltip from '../../../../../shared/components/tooltip'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { UserEmailData } from '../../../../../../../types/user-email' import { UserEmailData } from '../../../../../../../types/user-email'
import { useUserEmailsContext } from '../../../context/user-email-context' import { useUserEmailsContext } from '../../../context/user-email-context'
import { postJSON } from '../../../../../infrastructure/fetch-json' import { postJSON } from '../../../../../infrastructure/fetch-json'
import { UseAsyncReturnType } from '../../../../../shared/hooks/use-async' import { UseAsyncReturnType } from '../../../../../shared/hooks/use-async'
import TooltipWrapper from '@/features/ui/components/bootstrap-5/wrappers/tooltip-wrapper'
function DeleteButton({ children, disabled, onClick }: Button.ButtonProps) { function DeleteButton({ disabled, onClick }: Button.ButtonProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
@ -53,7 +53,7 @@ function Remove({ userEmailData, deleteEmailAsync }: RemoveProps) {
} }
return ( return (
<Tooltip <TooltipWrapper
id={userEmailData.email} id={userEmailData.email}
description={ description={
userEmailData.default userEmailData.default
@ -68,7 +68,7 @@ function Remove({ userEmailData, deleteEmailAsync }: RemoveProps) {
onClick={handleRemoveUserEmail} onClick={handleRemoveUserEmail}
/> />
</span> </span>
</Tooltip> </TooltipWrapper>
) )
} }

View file

@ -0,0 +1,86 @@
import { cloneElement, useEffect, forwardRef } from 'react'
import { OverlayTrigger, Tooltip as BSTooltip } from 'react-bootstrap-5'
import { callFnsInSequence } from '@/utils/functions'
type OverlayProps = Omit<
React.ComponentProps<typeof OverlayTrigger>,
'overlay' | 'children'
>
type UpdatingTooltipProps = {
popper: {
scheduleUpdate: () => void
}
show: boolean
[x: string]: unknown
}
const UpdatingTooltip = forwardRef<HTMLDivElement, UpdatingTooltipProps>(
({ popper, children, show: _, ...props }, ref) => {
useEffect(() => {
popper.scheduleUpdate()
}, [children, popper])
return (
<BSTooltip ref={ref} {...props}>
{children}
</BSTooltip>
)
}
)
UpdatingTooltip.displayName = 'UpdatingTooltip'
export type TooltipProps = {
description: React.ReactNode
id: string
overlayProps?: OverlayProps
tooltipProps?: React.ComponentProps<typeof BSTooltip>
hidden?: boolean
children: React.ReactElement
}
function Tooltip({
id,
description,
children,
tooltipProps,
overlayProps,
hidden,
}: TooltipProps) {
const delay = overlayProps?.delay
let delayShow = 300
let delayHide = 300
if (delay) {
delayShow = typeof delay === 'number' ? delay : delay.show
delayHide = typeof delay === 'number' ? delay : delay.hide
}
const hideTooltip = (e: React.MouseEvent) => {
if (e.currentTarget instanceof HTMLElement) {
e.currentTarget.blur()
}
}
return (
<OverlayTrigger
overlay={
<UpdatingTooltip
id={`${id}-tooltip`}
{...tooltipProps}
style={{ display: hidden ? 'none' : 'block' }}
>
{description}
</UpdatingTooltip>
}
{...overlayProps}
delay={{ show: delayShow, hide: delayHide }}
placement={overlayProps?.placement || 'top'}
>
{cloneElement(children, {
onClick: callFnsInSequence(children.props.onClick, hideTooltip),
})}
</OverlayTrigger>
)
}
export default Tooltip

View file

@ -0,0 +1,41 @@
import Tooltip from '@/features/ui/components/bootstrap-5/tooltip'
import BS3Tooltip from '@/shared/components/tooltip'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type TooltipWrapperProps = React.ComponentProps<typeof Tooltip> & {
bs3Props?: Record<string, unknown>
}
function TooltipWrapper(props: TooltipWrapperProps) {
const { bs3Props, ...bs5Props } = props
const bs3TooltipProps: React.ComponentProps<typeof BS3Tooltip> = {
children: bs5Props.children,
id: bs5Props.id,
description: bs5Props.description,
overlayProps: {},
...bs3Props,
}
if ('hidden' in bs5Props) {
bs3TooltipProps.hidden = bs5Props.hidden
}
const delay = bs5Props.overlayProps?.delay
if (delay && typeof delay !== 'number') {
bs3TooltipProps.overlayProps = {
...bs3TooltipProps.overlayProps,
delayShow: delay.show,
delayHide: delay.hide,
}
}
return (
<BootstrapVersionSwitcher
bs3={<BS3Tooltip {...bs3TooltipProps} />}
bs5={<Tooltip {...bs5Props} />}
/>
)
}
export default TooltipWrapper

View file

@ -0,0 +1,41 @@
import { Button } from 'react-bootstrap-5'
import Tooltip from '@/features/ui/components/bootstrap-5/tooltip'
import { Meta } from '@storybook/react'
export const Tooltips = () => {
const placements = ['top', 'right', 'bottom', 'left'] as const
return (
<div
style={{
width: '200px',
display: 'flex',
flexDirection: 'column',
margin: '0 auto',
padding: '35px 0',
gap: '35px',
}}
>
{placements.map(placement => (
<Tooltip
key={placement}
id={`tooltip-${placement}`}
description={`Tooltip on ${placement}`}
overlayProps={{ placement }}
>
<Button variant="secondary">Tooltip on {placement}</Button>
</Tooltip>
))}
</div>
)
}
const meta: Meta<typeof Tooltip> = {
title: 'Shared / Components / Bootstrap 5 / Tooltip',
component: Tooltip,
parameters: {
bootstrap5: true,
},
}
export default meta

View file

@ -21,3 +21,9 @@ $btn-border-radius-sm: $border-radius-full;
// Colors // Colors
$primary: $bg-accent-01; $primary: $bg-accent-01;
$secondary: $bg-light-primary; $secondary: $bg-light-primary;
// Tooltips
$tooltip-max-width: 320px;
$tooltip-border-radius: $border-radius-base;
$tooltip-padding-y: $spacing-04;
$tooltip-padding-x: $spacing-06;

View file

@ -29,6 +29,7 @@
@import 'bootstrap-5/scss/buttons'; @import 'bootstrap-5/scss/buttons';
@import 'bootstrap-5/scss/dropdown'; @import 'bootstrap-5/scss/dropdown';
@import 'bootstrap-5/scss/modal'; @import 'bootstrap-5/scss/modal';
@import 'bootstrap-5/scss/tooltip';
@import 'bootstrap-5/scss/spinners'; @import 'bootstrap-5/scss/spinners';
// Helpers // Helpers

View file

@ -2,3 +2,4 @@
@import 'dropdown-menu'; @import 'dropdown-menu';
@import 'split-button'; @import 'split-button';
@import 'notifications'; @import 'notifications';
@import 'tooltip';

View file

@ -0,0 +1,27 @@
.tooltip {
line-height: 20px;
@include shadow-md;
&.#{$prefix}tooltip-top {
bottom: -1px !important;
}
&.#{$prefix}tooltip-end {
top: 1px !important;
left: -1px !important;
}
&.#{$prefix}tooltip-bottom {
top: -1px !important;
}
&.#{$prefix}tooltip-start {
top: 1px !important;
right: -1px !important;
}
}
.tooltip-inner {
text-align: initial;
.tooltip-wide & {
max-width: unset;
}
}