فهرست منبع

Merge pull request #7956 from overleaf/ii-refactor-toolitp-usage

Tooltip usage refactoring

GitOrigin-RevId: f4b2d4d57722172141a081dd60e4394ff7fff332
Timothée Alby 4 سال پیش
والد
کامیت
5731463d32
20فایلهای تغییر یافته به همراه253 افزوده شده و 276 حذف شده
  1. 7 10
      services/web/frontend/js/features/editor-navigation-toolbar/components/back-to-projects-button.tsx
  2. 13 14
      services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.js
  3. 12 12
      services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.js
  4. 12 13
      services/web/frontend/js/features/editor-navigation-toolbar/components/pdf-toggle-button.tsx
  5. 18 19
      services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.tsx
  6. 34 23
      services/web/frontend/js/features/file-tree/components/file-tree-toolbar.js
  7. 19 15
      services/web/frontend/js/features/pdf-preview/components/pdf-compile-button-inner.tsx
  8. 9 7
      services/web/frontend/js/features/pdf-preview/components/pdf-expand-button.tsx
  9. 8 14
      services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx
  10. 7 7
      services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.tsx
  11. 12 17
      services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.js
  12. 9 11
      services/web/frontend/js/features/preview/components/preview-log-entry-header.js
  13. 11 18
      services/web/frontend/js/features/share-project-modal/components/edit-member.js
  14. 9 9
      services/web/frontend/js/features/share-project-modal/components/invite.js
  15. 10 9
      services/web/frontend/js/features/share-project-modal/components/link-sharing.js
  16. 0 35
      services/web/frontend/js/shared/components/beta-badge.js
  17. 39 0
      services/web/frontend/js/shared/components/beta-badge.tsx
  18. 18 17
      services/web/frontend/js/shared/components/copy-link.tsx
  19. 0 24
      services/web/frontend/js/shared/components/tooltip-button.js
  20. 6 2
      services/web/frontend/js/shared/components/tooltip.tsx

+ 7 - 10
services/web/frontend/js/features/editor-navigation-toolbar/components/back-to-projects-button.js → services/web/frontend/js/features/editor-navigation-toolbar/components/back-to-projects-button.tsx

@@ -1,18 +1,15 @@
-import Icon from '../../../shared/components/icon'
 import { useTranslation } from 'react-i18next'
-import { OverlayTrigger, Tooltip } from 'react-bootstrap'
+import Tooltip from '../../../shared/components/tooltip'
+import Icon from '../../../shared/components/icon'
 
 function BackToProjectsButton() {
   const { t } = useTranslation()
 
   return (
-    <OverlayTrigger
-      placement="right"
-      overlay={
-        <Tooltip id="back-to-projects-tooltip">
-          {t('back_to_your_projects')}
-        </Tooltip>
-      }
+    <Tooltip
+      id="back-to-projects"
+      description={t('back_to_your_projects')}
+      overlayProps={{ placement: 'right' }}
     >
       <div className="toolbar-item">
         <a className="btn btn-full-height" href="/project">
@@ -23,7 +20,7 @@ function BackToProjectsButton() {
           />
         </a>
       </div>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }
 

+ 13 - 14
services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.js

@@ -1,7 +1,8 @@
 import { useCallback } from 'react'
 import PropTypes from 'prop-types'
-import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
+import { Dropdown, MenuItem } from 'react-bootstrap'
 import { Trans, useTranslation } from 'react-i18next'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import IconChecked from '../../../shared/components/icon-checked'
 import ControlledDropdown from '../../../shared/components/controlled-dropdown'
@@ -50,21 +51,19 @@ function PdfDetachMenuItem({ handleDetach, children }) {
 
   if (!('BroadcastChannel' in window)) {
     return (
-      <OverlayTrigger
-        placement="left"
-        overlay={
-          <Tooltip id="detach-disabled-tooltip">
-            {t('your_browser_does_not_support_this_feature')}
-          </Tooltip>
-        }
+      <Tooltip
+        id="detach-disabled"
+        description={t('your_browser_does_not_support_this_feature')}
+        overlayProps={{ placement: 'left' }}
       >
         <MenuItem disabled>{children}</MenuItem>
-      </OverlayTrigger>
+      </Tooltip>
     )
   }
 
   return <MenuItem onSelect={handleDetach}>{children}</MenuItem>
 }
+
 PdfDetachMenuItem.propTypes = {
   handleDetach: PropTypes.func.isRequired,
   children: PropTypes.arrayOf(PropTypes.node).isRequired,
@@ -126,13 +125,13 @@ function LayoutDropdownButton() {
         <Dropdown.Toggle className="btn-full-height" bsStyle="link">
           {processing ? <IconRefresh /> : <IconLayout />}
           <span className="toolbar-label">{t('layout')}</span>
-          <OverlayTrigger
-            placement="bottom"
-            overlay={<Tooltip id="pdf-detach-badge">Beta feature</Tooltip>}
-            delayHide={100}
+          <Tooltip
+            id="pdf-detach-badge"
+            description="Beta feature"
+            overlayProps={{ placement: 'bottom', delayHide: 100 }}
           >
             <span className="beta-badge" />
-          </OverlayTrigger>
+          </Tooltip>
         </Dropdown.Toggle>
         <Dropdown.Menu id="layout-dropdown-list">
           <MenuItem onSelect={() => handleChangeLayout('sideBySide')}>

+ 12 - 12
services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.js

@@ -1,7 +1,8 @@
 import React from 'react'
 import PropTypes from 'prop-types'
 import { useTranslation } from 'react-i18next'
-import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
+import { Dropdown, MenuItem } from 'react-bootstrap'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import { getHueForUserId } from '../../../shared/utils/colors'
 import ControlledDropdown from '../../../shared/components/controlled-dropdown'
@@ -37,17 +38,17 @@ function OnlineUsersWidget({ onlineUsers, goToUser }) {
     return (
       <div className="online-users">
         {onlineUsers.map((user, index) => (
-          <OverlayTrigger
+          <Tooltip
             key={`${user.user_id}_${index}`}
-            placement="bottom"
-            trigger={['hover', 'focus']}
-            overlay={<Tooltip id="tooltip-online-user">{user.name}</Tooltip>}
+            id="online-user"
+            description={user.name}
+            overlayProps={{ placement: 'bottom', trigger: ['hover', 'focus'] }}
           >
             <span>
               {/* OverlayTrigger won't fire unless UserIcon is wrapped in a span */}
               <UserIcon user={user} onClick={goToUser} />
             </span>
-          </OverlayTrigger>
+          </Tooltip>
         ))}
       </div>
     )
@@ -94,11 +95,10 @@ UserIcon.propTypes = {
 const DropDownToggleButton = React.forwardRef((props, ref) => {
   const { t } = useTranslation()
   return (
-    <OverlayTrigger
-      placement="left"
-      overlay={
-        <Tooltip id="tooltip-connected-users">{t('connected_users')}</Tooltip>
-      }
+    <Tooltip
+      id="connected-users"
+      description={t('connected_users')}
+      overlayProps={{ placement: 'left' }}
     >
       <button
         className="btn online-user online-user-multi"
@@ -107,7 +107,7 @@ const DropDownToggleButton = React.forwardRef((props, ref) => {
         <strong>{props.onlineUserCount}</strong>
         <Icon type="users" fw />
       </button>
-    </OverlayTrigger>
+    </Tooltip>
   )
 })
 

+ 12 - 13
services/web/frontend/js/features/editor-navigation-toolbar/components/pdf-toggle-button.js → services/web/frontend/js/features/editor-navigation-toolbar/components/pdf-toggle-button.tsx

@@ -1,9 +1,13 @@
-import PropTypes from 'prop-types'
-import { OverlayTrigger, Tooltip } from 'react-bootstrap'
 import classNames from 'classnames'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 
-function PdfToggleButton({ onClick, pdfViewIsOpen }) {
+type PdfToggleButtonProps = {
+  onClick: () => void
+  pdfViewIsOpen?: boolean
+}
+
+function PdfToggleButton({ onClick, pdfViewIsOpen }: PdfToggleButtonProps) {
   const classes = classNames(
     'btn',
     'btn-full-height',
@@ -14,22 +18,17 @@ function PdfToggleButton({ onClick, pdfViewIsOpen }) {
   )
 
   return (
-    <OverlayTrigger
-      placement="bottom"
-      trigger={['hover', 'focus']}
-      overlay={<Tooltip id="tooltip-online-user">PDF</Tooltip>}
+    <Tooltip
+      id="online-user"
+      description="PDF"
+      overlayProps={{ placement: 'bottom', trigger: ['hover', 'focus'] }}
     >
       {/* eslint-disable-next-line jsx-a11y/anchor-is-valid,jsx-a11y/click-events-have-key-events,jsx-a11y/interactive-supports-focus */}
       <a role="button" className={classes} onClick={onClick}>
         <Icon type="file-pdf-o" fw accessibilityLabel="PDF" />
       </a>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }
 
-PdfToggleButton.propTypes = {
-  onClick: PropTypes.func.isRequired,
-  pdfViewIsOpen: PropTypes.bool,
-}
-
 export default PdfToggleButton

+ 18 - 19
services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.js → services/web/frontend/js/features/editor-navigation-toolbar/components/project-name-editable-label.tsx

@@ -1,16 +1,22 @@
 import { useEffect, useState, useRef } from 'react'
-import PropTypes from 'prop-types'
-import { OverlayTrigger, Tooltip } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import classNames from 'classnames'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 
+type ProjectNameEditableLabelProps = {
+  projectName: string
+  onChange: (value: string) => void
+  hasRenamePermissions?: boolean
+  className?: string
+}
+
 function ProjectNameEditableLabel({
   projectName,
   hasRenamePermissions,
   onChange,
   className,
-}) {
+}: ProjectNameEditableLabelProps) {
   const { t } = useTranslation()
 
   const [isRenaming, setIsRenaming] = useState(false)
@@ -19,11 +25,11 @@ function ProjectNameEditableLabel({
 
   const [inputContent, setInputContent] = useState(projectName)
 
-  const inputRef = useRef(null)
+  const inputRef = useRef<HTMLInputElement | null>(null)
 
   useEffect(() => {
     if (isRenaming) {
-      inputRef.current.select()
+      inputRef.current?.select()
     }
   }, [isRenaming])
 
@@ -39,14 +45,14 @@ function ProjectNameEditableLabel({
     onChange(inputContent)
   }
 
-  function handleKeyDown(event) {
+  function handleKeyDown(event: React.KeyboardEvent) {
     if (event.key === 'Enter') {
       event.preventDefault()
       finishRenaming()
     }
   }
 
-  function handleOnChange(event) {
+  function handleOnChange(event: React.ChangeEvent<HTMLInputElement>) {
     setInputContent(event.target.value)
   }
 
@@ -75,26 +81,19 @@ function ProjectNameEditableLabel({
         />
       )}
       {canRename && (
-        <OverlayTrigger
-          placement="bottom"
-          trigger={['hover', 'focus']}
-          overlay={<Tooltip id="tooltip-online-user">{t('rename')}</Tooltip>}
+        <Tooltip
+          id="online-user"
+          description={t('rename')}
+          overlayProps={{ placement: 'bottom', trigger: ['hover', 'focus'] }}
         >
           {/* eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */}
           <a className="rename" role="button" onClick={startRenaming}>
             <Icon type="pencil" fw />
           </a>
-        </OverlayTrigger>
+        </Tooltip>
       )}
     </div>
   )
 }
 
-ProjectNameEditableLabel.propTypes = {
-  projectName: PropTypes.string.isRequired,
-  hasRenamePermissions: PropTypes.bool,
-  onChange: PropTypes.func.isRequired,
-  className: PropTypes.string,
-}
-
 export default ProjectNameEditableLabel

+ 34 - 23
services/web/frontend/js/features/file-tree/components/file-tree-toolbar.js

@@ -1,8 +1,9 @@
 import PropTypes from 'prop-types'
 import { useTranslation } from 'react-i18next'
 
+import { Button } from 'react-bootstrap'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
-import TooltipButton from '../../../shared/components/tooltip-button'
 
 import { useEditorContext } from '../../../shared/context/editor-context'
 import { useFileTreeActionable } from '../contexts/file-tree-actionable'
@@ -37,27 +38,33 @@ function FileTreeToolbarLeft() {
 
   return (
     <div className="toolbar-left">
-      <TooltipButton
-        id="new_file"
+      <Tooltip
+        id="new-file"
         description={t('new_file')}
-        onClick={startCreatingDocOrFile}
+        overlayProps={{ placement: 'bottom' }}
       >
-        <Icon type="file" fw accessibilityLabel={t('new_file')} />
-      </TooltipButton>
-      <TooltipButton
-        id="new_folder"
+        <Button onClick={startCreatingDocOrFile}>
+          <Icon type="file" fw accessibilityLabel={t('new_file')} />
+        </Button>
+      </Tooltip>
+      <Tooltip
+        id="new-folder"
         description={t('new_folder')}
-        onClick={startCreatingFolder}
+        overlayProps={{ placement: 'bottom' }}
       >
-        <Icon type="folder" fw accessibilityLabel={t('new_folder')} />
-      </TooltipButton>
-      <TooltipButton
+        <Button onClick={startCreatingFolder}>
+          <Icon type="folder" fw accessibilityLabel={t('new_folder')} />
+        </Button>
+      </Tooltip>
+      <Tooltip
         id="upload"
         description={t('upload')}
-        onClick={startUploadingDocOrFile}
+        overlayProps={{ placement: 'bottom' }}
       >
-        <Icon type="upload" fw accessibilityLabel={t('upload')} />
-      </TooltipButton>
+        <Button onClick={startUploadingDocOrFile}>
+          <Icon type="upload" fw accessibilityLabel={t('upload')} />
+        </Button>
+      </Tooltip>
     </div>
   )
 }
@@ -74,22 +81,26 @@ function FileTreeToolbarRight() {
   return (
     <div className="toolbar-right">
       {canRename ? (
-        <TooltipButton
+        <Tooltip
           id="rename"
           description={t('rename')}
-          onClick={startRenaming}
+          overlayProps={{ placement: 'bottom' }}
         >
-          <Icon type="pencil" fw accessibilityLabel={t('rename')} />
-        </TooltipButton>
+          <Button onClick={startRenaming}>
+            <Icon type="pencil" fw accessibilityLabel={t('rename')} />
+          </Button>
+        </Tooltip>
       ) : null}
       {canDelete ? (
-        <TooltipButton
+        <Tooltip
           id="delete"
           description={t('delete')}
-          onClick={startDeleting}
+          overlayProps={{ placement: 'bottom' }}
         >
-          <Icon type="trash-o" fw accessibilityLabel={t('delete')} />
-        </TooltipButton>
+          <Button onClick={startDeleting}>
+            <Icon type="trash-o" fw accessibilityLabel={t('delete')} />
+          </Button>
+        </Tooltip>
       ) : null}
     </div>
   )

+ 19 - 15
services/web/frontend/js/features/pdf-preview/components/pdf-compile-button-inner.js → services/web/frontend/js/features/pdf-preview/components/pdf-compile-button-inner.tsx

@@ -1,26 +1,35 @@
-import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
-import PropTypes from 'prop-types'
+import { Button } from 'react-bootstrap'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import { useTranslation } from 'react-i18next'
 import { memo } from 'react'
 
 const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
 
-function PdfCompileButtonInner({ startCompile, compiling }) {
+type PdfCompileButtonInnerProps = {
+  startCompile: () => void
+  compiling: boolean
+}
+
+function PdfCompileButtonInner({
+  startCompile,
+  compiling,
+}: PdfCompileButtonInnerProps) {
   const { t } = useTranslation()
 
   const compileButtonLabel = compiling ? t('compiling') + '…' : t('recompile')
 
   return (
-    <OverlayTrigger
-      placement="bottom"
-      delayShow={500}
-      overlay={
-        <Tooltip id="tooltip-logs-toggle" className="keyboard-tooltip">
+    <Tooltip
+      id="logs-toggle"
+      description={
+        <>
           {t('recompile_pdf')}{' '}
           <span className="keyboard-shortcut">({modifierKey} + Enter)</span>
-        </Tooltip>
+        </>
       }
+      tooltipProps={{ className: 'keyboard-tooltip' }}
+      overlayProps={{ delayShow: 500 }}
     >
       <Button
         className="btn-recompile"
@@ -34,13 +43,8 @@ function PdfCompileButtonInner({ startCompile, compiling }) {
           {compileButtonLabel}
         </span>
       </Button>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }
 
-PdfCompileButtonInner.propTypes = {
-  compiling: PropTypes.bool.isRequired,
-  startCompile: PropTypes.func.isRequired,
-}
-
 export default memo(PdfCompileButtonInner)

+ 9 - 7
services/web/frontend/js/features/pdf-preview/components/pdf-expand-button.js → services/web/frontend/js/features/pdf-preview/components/pdf-expand-button.tsx

@@ -1,7 +1,8 @@
-import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
+import { Button } from 'react-bootstrap'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
-import { memo, useMemo } from 'react'
+import { useMemo } from 'react'
 import { useLayoutContext } from '../../../shared/context/layout-context'
 
 function PdfExpandButton() {
@@ -14,9 +15,10 @@ function PdfExpandButton() {
   }, [pdfLayout, t])
 
   return (
-    <OverlayTrigger
-      placement="left"
-      overlay={<Tooltip id="expand-pdf-btn">{text}</Tooltip>}
+    <Tooltip
+      id="expand-pdf-btn"
+      description="text"
+      overlayProps={{ placement: 'left' }}
     >
       <Button
         onClick={switchLayout}
@@ -25,8 +27,8 @@ function PdfExpandButton() {
       >
         <Icon type={pdfLayout === 'sideBySide' ? 'expand' : 'compress'} />
       </Button>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }
 
-export default memo(PdfExpandButton)
+export default PdfExpandButton

+ 8 - 14
services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.js → services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx

@@ -1,25 +1,19 @@
 import { useTranslation } from 'react-i18next'
-import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
+import { Button } from 'react-bootstrap'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
-import { memo } from 'react'
 import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
 
 function PdfHybridDownloadButton() {
   const { pdfDownloadUrl } = useCompileContext()
 
   const { t } = useTranslation()
+  const description = pdfDownloadUrl
+    ? t('download_pdf')
+    : t('please_compile_pdf_before_download')
 
   return (
-    <OverlayTrigger
-      placement="bottom"
-      overlay={
-        <Tooltip id="tooltip-logs-toggle">
-          {pdfDownloadUrl
-            ? t('download_pdf')
-            : t('please_compile_pdf_before_download')}
-        </Tooltip>
-      }
-    >
+    <Tooltip id="logs-toggle" description={description}>
       <Button
         bsStyle="link"
         disabled={!pdfDownloadUrl}
@@ -30,8 +24,8 @@ function PdfHybridDownloadButton() {
       >
         <Icon type="download" fw />
       </Button>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }
 
-export default memo(PdfHybridDownloadButton)
+export default PdfHybridDownloadButton

+ 7 - 7
services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.js → services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.tsx

@@ -1,6 +1,7 @@
 import { memo, useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
-import { Button, Label, OverlayTrigger, Tooltip } from 'react-bootstrap'
+import { Button, Label } from 'react-bootstrap'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
 
@@ -18,11 +19,10 @@ function PdfHybridLogsButton() {
   const totalCount = errorCount + warningCount
 
   return (
-    <OverlayTrigger
-      placement="bottom"
-      overlay={
-        <Tooltip id="tooltip-logs-toggle">{t('logs_and_output_files')}</Tooltip>
-      }
+    <Tooltip
+      id="logs-toggle"
+      description={t('logs_and_output_files')}
+      overlayProps={{ placement: 'bottom' }}
     >
       <Button
         bsStyle="link"
@@ -41,7 +41,7 @@ function PdfHybridLogsButton() {
           </Label>
         )}
       </Button>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }
 

+ 12 - 17
services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.js

@@ -7,7 +7,8 @@ import { getJSON } from '../../../infrastructure/fetch-json'
 import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
 import { useLayoutContext } from '../../../shared/context/layout-context'
 import useScopeValue from '../../../shared/hooks/use-scope-value'
-import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
+import { Button } from 'react-bootstrap'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import { useTranslation } from 'react-i18next'
 import useIsMounted from '../../../shared/hooks/use-is-mounted'
@@ -38,13 +39,10 @@ function GoToCodeButton({
   }
 
   return (
-    <OverlayTrigger
-      placement={tooltipPlacement}
-      overlay={
-        <Tooltip id="sync-to-code-tooltip">
-          {t('go_to_pdf_location_in_code')}
-        </Tooltip>
-      }
+    <Tooltip
+      id="sync-to-code"
+      description={t('go_to_pdf_location_in_code')}
+      overlayProps={{ placement: tooltipPlacement }}
     >
       <Button
         bsStyle="default"
@@ -57,7 +55,7 @@ function GoToCodeButton({
         {buttonIcon}
         {isDetachLayout ? <span>&nbsp;{t('show_in_code')}</span> : ''}
       </Button>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }
 
@@ -82,13 +80,10 @@ function GoToPdfButton({
   }
 
   return (
-    <OverlayTrigger
-      placement={tooltipPlacement}
-      overlay={
-        <Tooltip id="sync-to-pdf-tooltip">
-          {t('go_to_code_location_in_pdf')}
-        </Tooltip>
-      }
+    <Tooltip
+      id="sync-to-pdf"
+      description={t('go_to_code_location_in_pdf')}
+      overlayProps={{ placement: tooltipPlacement }}
     >
       <Button
         bsStyle="default"
@@ -101,7 +96,7 @@ function GoToPdfButton({
         {buttonIcon}
         {isDetachLayout ? <span>&nbsp;{t('show_in_pdf')}</span> : ''}
       </Button>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }
 

+ 9 - 11
services/web/frontend/js/features/preview/components/preview-log-entry-header.js

@@ -2,8 +2,8 @@ import PropTypes from 'prop-types'
 import classNames from 'classnames'
 import { useState, useRef } from 'react'
 import { useTranslation } from 'react-i18next'
-import { OverlayTrigger, Tooltip } from 'react-bootstrap'
 import useResizeObserver from '../hooks/use-resize-observer'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 
 function PreviewLogEntryHeader({
@@ -84,13 +84,6 @@ function PreviewLogEntryHeader({
     </button>
   ) : null
 
-  const locationTooltip =
-    locationSpanOverflown && locationLinkText ? (
-      <Tooltip id={locationLinkText} className="log-location-tooltip">
-        {locationLinkText}
-      </Tooltip>
-    ) : null
-
   const headerTitleText = logType ? `${logType} ${headerTitle}` : headerTitle
 
   return (
@@ -99,10 +92,15 @@ function PreviewLogEntryHeader({
         <div className="log-entry-header-icon-container">{headerIcon}</div>
       ) : null}
       <h3 className="log-entry-header-title">{headerTitleText}</h3>
-      {locationTooltip ? (
-        <OverlayTrigger placement="left" overlay={locationTooltip}>
+      {locationSpanOverflown && locationLinkText ? (
+        <Tooltip
+          id={locationLinkText}
+          description={locationLinkText}
+          overlayProps={{ placement: 'left' }}
+          tooltipProps={{ className: 'log-location-tooltip' }}
+        >
           {locationLink}
-        </OverlayTrigger>
+        </Tooltip>
       ) : (
         locationLink
       )}

+ 11 - 18
services/web/frontend/js/features/share-project-modal/components/edit-member.js

@@ -2,18 +2,11 @@ import { useState, useEffect } from 'react'
 import PropTypes from 'prop-types'
 import { Trans, useTranslation } from 'react-i18next'
 import { useShareProjectContext } from './share-project-modal'
-import Icon from '../../../shared/components/icon'
 import TransferOwnershipModal from './transfer-ownership-modal'
-import {
-  Button,
-  Col,
-  Form,
-  FormControl,
-  FormGroup,
-  OverlayTrigger,
-  Tooltip,
-} from 'react-bootstrap'
 import { removeMemberFromProject, updateMember } from '../utils/api'
+import { Button, Col, Form, FormControl, FormGroup } from 'react-bootstrap'
+import Tooltip from '../../../shared/components/tooltip'
+import Icon from '../../../shared/components/icon'
 import { useProjectContext } from '../../../shared/context/project-context'
 
 export default function EditMember({ member }) {
@@ -110,6 +103,7 @@ function SelectPrivilege({ value, handleChange }) {
     </FormControl>
   )
 }
+
 SelectPrivilege.propTypes = {
   value: PropTypes.string.isRequired,
   handleChange: PropTypes.func.isRequired,
@@ -134,13 +128,10 @@ function RemoveMemberAction({ member }) {
 
   return (
     <FormControl.Static className="text-center">
-      <OverlayTrigger
-        placement="bottom"
-        overlay={
-          <Tooltip id="tooltip-remove-collaborator">
-            <Trans i18nKey="remove_collaborator" />
-          </Tooltip>
-        }
+      <Tooltip
+        id="remove-collaborator"
+        description={<Trans i18nKey="remove_collaborator" />}
+        overlayProps={{ placement: 'bottom' }}
       >
         <Button
           type="button"
@@ -151,10 +142,11 @@ function RemoveMemberAction({ member }) {
         >
           <Icon type="times" />
         </Button>
-      </OverlayTrigger>
+      </Tooltip>
     </FormControl.Static>
   )
 }
+
 RemoveMemberAction.propTypes = {
   member: PropTypes.shape({
     _id: PropTypes.string.isRequired,
@@ -179,6 +171,7 @@ function ChangePrivilegesActions({ handleReset }) {
     </div>
   )
 }
+
 ChangePrivilegesActions.propTypes = {
   handleReset: PropTypes.func.isRequired,
 }

+ 9 - 9
services/web/frontend/js/features/share-project-modal/components/invite.js

@@ -2,7 +2,8 @@ import { useCallback } from 'react'
 import PropTypes from 'prop-types'
 import { useShareProjectContext } from './share-project-modal'
 import Icon from '../../../shared/components/icon'
-import { Button, Col, Row, OverlayTrigger, Tooltip } from 'react-bootstrap'
+import { Button, Col, Row } from 'react-bootstrap'
+import Tooltip from '../../../shared/components/tooltip'
 import { Trans, useTranslation } from 'react-i18next'
 import MemberPrivileges from './member-privileges'
 import { resendInvite, revokeInvite } from '../utils/api'
@@ -68,6 +69,7 @@ function ResendInvite({ invite }) {
     </Button>
   )
 }
+
 ResendInvite.propTypes = {
   invite: PropTypes.object.isRequired,
 }
@@ -88,13 +90,10 @@ function RevokeInvite({ invite }) {
   }
 
   return (
-    <OverlayTrigger
-      placement="bottom"
-      overlay={
-        <Tooltip id="tooltip-revoke-invite">
-          <Trans i18nKey="revoke_invite" />
-        </Tooltip>
-      }
+    <Tooltip
+      id="revoke-invite"
+      description={<Trans i18nKey="revoke_invite" />}
+      overlayProps={{ placement: 'bottom' }}
     >
       <Button
         type="button"
@@ -105,9 +104,10 @@ function RevokeInvite({ invite }) {
       >
         <Icon type="times" />
       </Button>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }
+
 RevokeInvite.propTypes = {
   invite: PropTypes.object.isRequired,
 }

+ 10 - 9
services/web/frontend/js/features/share-project-modal/components/link-sharing.js

@@ -1,7 +1,8 @@
 import { useCallback, useState } from 'react'
 import PropTypes from 'prop-types'
-import { Button, Col, OverlayTrigger, Row, Tooltip } from 'react-bootstrap'
+import { Button, Col, Row } from 'react-bootstrap'
 import { Trans } from 'react-i18next'
+import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 import { useShareProjectContext } from './share-project-modal'
 import { setProjectAccessLevel } from '../utils/api'
@@ -93,6 +94,7 @@ function PrivateSharing({ setAccessLevel, inflight }) {
     </Row>
   )
 }
+
 PrivateSharing.propTypes = {
   setAccessLevel: PropTypes.func.isRequired,
   inflight: PropTypes.bool,
@@ -144,6 +146,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
     </Row>
   )
 }
+
 TokenBasedSharing.propTypes = {
   setAccessLevel: PropTypes.func.isRequired,
   inflight: PropTypes.bool,
@@ -177,6 +180,7 @@ function LegacySharing({ accessLevel, setAccessLevel, inflight }) {
     </Row>
   )
 }
+
 LegacySharing.propTypes = {
   accessLevel: PropTypes.string.isRequired,
   setAccessLevel: PropTypes.func.isRequired,
@@ -230,6 +234,7 @@ function AccessToken({ token, path, tooltipId }) {
     </pre>
   )
 }
+
 AccessToken.propTypes = {
   token: PropTypes.string,
   tooltipId: PropTypes.string.isRequired,
@@ -238,13 +243,9 @@ AccessToken.propTypes = {
 
 function LinkSharingInfo() {
   return (
-    <OverlayTrigger
-      placement="top"
-      overlay={
-        <Tooltip id="tooltip-link-sharing-info">
-          <Trans i18nKey="learn_more_about_link_sharing" />
-        </Tooltip>
-      }
+    <Tooltip
+      id="link-sharing-info"
+      description={<Trans i18nKey="learn_more_about_link_sharing" />}
     >
       <a
         href="/learn/how-to/What_is_Link_Sharing%3F"
@@ -253,6 +254,6 @@ function LinkSharingInfo() {
       >
         <Icon type="question-circle" />
       </a>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }

+ 0 - 35
services/web/frontend/js/shared/components/beta-badge.js

@@ -1,35 +0,0 @@
-import { OverlayTrigger, Tooltip } from 'react-bootstrap'
-import PropTypes from 'prop-types'
-
-export default function BetaBadge({ tooltip, url = '/beta/participate' }) {
-  return (
-    <OverlayTrigger
-      placement={tooltip.placement || 'bottom'}
-      overlay={
-        <Tooltip id={tooltip.id} className={tooltip.className}>
-          {tooltip.text}
-        </Tooltip>
-      }
-      delayHide={100}
-    >
-      <a
-        href={url}
-        target="_blank"
-        rel="noopener noreferrer"
-        className="badge beta-badge"
-      >
-        <span className="sr-only">{tooltip.text}</span>
-      </a>
-    </OverlayTrigger>
-  )
-}
-
-BetaBadge.propTypes = {
-  tooltip: PropTypes.shape({
-    id: PropTypes.string.isRequired,
-    text: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
-    placement: PropTypes.string,
-    className: PropTypes.string,
-  }),
-  url: PropTypes.string,
-}

+ 39 - 0
services/web/frontend/js/shared/components/beta-badge.tsx

@@ -0,0 +1,39 @@
+import Tooltip from './tooltip'
+import { OverlayTriggerProps } from 'react-bootstrap'
+
+type TooltipProps = {
+  id: string
+  text: React.ReactNode
+  placement?: OverlayTriggerProps['placement']
+  className?: string
+}
+
+type BetaBadgeProps = {
+  tooltip: TooltipProps
+  url?: string
+}
+
+function BetaBadge({ tooltip, url = '/beta/participate' }: BetaBadgeProps) {
+  return (
+    <Tooltip
+      id={tooltip.id}
+      description={tooltip.text}
+      tooltipProps={{ className: tooltip.className }}
+      overlayProps={{
+        placement: tooltip.placement || 'bottom',
+        delayHide: 100,
+      }}
+    >
+      <a
+        href={url}
+        target="_blank"
+        rel="noopener noreferrer"
+        className="badge beta-badge"
+      >
+        <span className="sr-only">{tooltip.text}</span>
+      </a>
+    </Tooltip>
+  )
+}
+
+export default BetaBadge

+ 18 - 17
services/web/frontend/js/shared/components/copy-link.js → services/web/frontend/js/shared/components/copy-link.tsx

@@ -1,10 +1,15 @@
 import { useCallback, useState } from 'react'
-import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
-import PropTypes from 'prop-types'
+import { Button } from 'react-bootstrap'
 import { Trans, useTranslation } from 'react-i18next'
+import Tooltip from './tooltip'
 import Icon from './icon'
 
-export default function CopyLink({ link, tooltipId }) {
+type CopyLinkProps = {
+  link: string
+  tooltipId: string
+}
+
+function CopyLink({ link, tooltipId }: CopyLinkProps) {
   const { t } = useTranslation()
 
   const [copied, setCopied] = useState(false)
@@ -23,15 +28,13 @@ export default function CopyLink({ link, tooltipId }) {
   }
 
   return (
-    <OverlayTrigger
-      placement="top"
-      delayHide={copied ? 1000 : 250}
-      shouldUpdatePosition
-      overlay={
-        <Tooltip id={tooltipId}>
-          {copied ? 'Copied!' : <Trans i18nKey="copy" />}
-        </Tooltip>
-      }
+    <Tooltip
+      id={tooltipId}
+      description={copied ? 'Copied!' : <Trans i18nKey="copy" />}
+      overlayProps={{
+        delayHide: copied ? 1000 : 250,
+        shouldUpdatePosition: true,
+      }}
     >
       <Button
         onClick={handleClick}
@@ -42,10 +45,8 @@ export default function CopyLink({ link, tooltipId }) {
       >
         {copied ? <Icon type="check" /> : <Icon type="clipboard" />}
       </Button>
-    </OverlayTrigger>
+    </Tooltip>
   )
 }
-CopyLink.propTypes = {
-  link: PropTypes.string.isRequired,
-  tooltipId: PropTypes.string.isRequired,
-}
+
+export default CopyLink

+ 0 - 24
services/web/frontend/js/shared/components/tooltip-button.js

@@ -1,24 +0,0 @@
-import PropTypes from 'prop-types'
-import { Button, Tooltip, OverlayTrigger } from 'react-bootstrap'
-
-function TooltipButton({ id, description, onClick, children }) {
-  const tooltip = <Tooltip id={`${id}_tooltip`}>{description}</Tooltip>
-
-  return (
-    <OverlayTrigger placement="bottom" overlay={tooltip}>
-      <Button onClick={onClick}>{children}</Button>
-    </OverlayTrigger>
-  )
-}
-
-TooltipButton.propTypes = {
-  id: PropTypes.string.isRequired,
-  description: PropTypes.string.isRequired,
-  onClick: PropTypes.func,
-  children: PropTypes.oneOfType([
-    PropTypes.arrayOf(PropTypes.node),
-    PropTypes.node,
-  ]),
-}
-
-export default TooltipButton

+ 6 - 2
services/web/frontend/js/shared/components/tooltip.tsx

@@ -4,11 +4,15 @@ import {
   Tooltip as BSTooltip,
 } from 'react-bootstrap'
 
+type OverlayTriggerCustomProps = {
+  shouldUpdatePosition?: boolean // Not officially documented https://stackoverflow.com/a/43138470
+} & OverlayTriggerProps
+
 type TooltipProps = {
   children: React.ReactNode
-  description: string
+  description: React.ReactNode
   id: string
-  overlayProps?: Omit<OverlayTriggerProps, 'overlay'>
+  overlayProps?: Omit<OverlayTriggerCustomProps, 'overlay'>
   tooltipProps?: BSTooltip.TooltipProps
 }