mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #17736 from overleaf/ii-bs5-tooltip
[web] Create Btoostrap 5 tooltips GitOrigin-RevId: 28c7c389bda74765482049750fc0ae8a5995968e
This commit is contained in:
parent
a0da44358f
commit
9f38436f10
9 changed files with 210 additions and 7 deletions
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
41
services/web/frontend/stories/ui/tooltip.stories.tsx
Normal file
41
services/web/frontend/stories/ui/tooltip.stories.tsx
Normal 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
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
@import 'dropdown-menu';
|
@import 'dropdown-menu';
|
||||||
@import 'split-button';
|
@import 'split-button';
|
||||||
@import 'notifications';
|
@import 'notifications';
|
||||||
|
@import 'tooltip';
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue