Просмотр исходного кода

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

[web] Create Btoostrap 5 tooltips

GitOrigin-RevId: 28c7c389bda74765482049750fc0ae8a5995968e
M Fahru 2 лет назад
Родитель
Сommit
9f38436f10

+ 3 - 3
services/web/frontend/js/features/settings/components/emails/actions/make-primary/make-primary.tsx

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

+ 4 - 4
services/web/frontend/js/features/settings/components/emails/actions/remove.tsx

@@ -1,13 +1,13 @@
 import Icon from '../../../../../shared/components/icon'
-import Tooltip from '../../../../../shared/components/tooltip'
 import { Button } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { UserEmailData } from '../../../../../../../types/user-email'
 import { useUserEmailsContext } from '../../../context/user-email-context'
 import { postJSON } from '../../../../../infrastructure/fetch-json'
 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()
 
   return (
@@ -53,7 +53,7 @@ function Remove({ userEmailData, deleteEmailAsync }: RemoveProps) {
   }
 
   return (
-    <Tooltip
+    <TooltipWrapper
       id={userEmailData.email}
       description={
         userEmailData.default
@@ -68,7 +68,7 @@ function Remove({ userEmailData, deleteEmailAsync }: RemoveProps) {
           onClick={handleRemoveUserEmail}
         />
       </span>
-    </Tooltip>
+    </TooltipWrapper>
   )
 }
 

+ 86 - 0
services/web/frontend/js/features/ui/components/bootstrap-5/tooltip.tsx

@@ -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

+ 41 - 0
services/web/frontend/js/features/ui/components/bootstrap-5/wrappers/tooltip-wrapper.tsx

@@ -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 - 0
services/web/frontend/stories/ui/tooltip.stories.tsx

@@ -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

+ 6 - 0
services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss

@@ -21,3 +21,9 @@ $btn-border-radius-sm: $border-radius-full;
 // Colors
 $primary: $bg-accent-01;
 $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;

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/bootstrap.scss

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

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/components/all.scss

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

+ 27 - 0
services/web/frontend/stylesheets/bootstrap-5/components/tooltip.scss

@@ -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;
+  }
+}