Przeglądaj źródła

Merge pull request #21832 from overleaf/jel-search-badges

[web] Add tooltips when needed on badge links in gallery search results

GitOrigin-RevId: edcdcf8134698f17f607e003627a8b4123519b0b
Jessica Lawshe 1 rok temu
rodzic
commit
5c63188200

+ 2 - 4
services/web/frontend/js/features/tooltip/index-bs5.ts

@@ -17,9 +17,7 @@ const footerLanguageElement = document.querySelector(
 
 const allTooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]')
 
-const possibleBadgeTooltips = document.querySelectorAll(
-  '[data-bs-toggle="tooltip-if-needed"]'
-)
+const possibleBadgeTooltips = document.querySelectorAll('[data-badge-tooltip]')
 
 // eslint-disable-next-line no-unused-vars
 const footLangTooltip = new Tooltip(footerLanguageElement)
@@ -30,7 +28,7 @@ allTooltips.forEach(element => {
 })
 
 possibleBadgeTooltips.forEach(element => {
-  // Put data-bs-toggle="tooltip-if-needed" on .badge-content
+  // Put data-badge-tooltip on .badge-content
   // then tooltip is only shown if content is clipped due to max-width on .badge
   // Due to font loading, the width calculated on page load might change, so we might
   // incorrectly determine a tooltip is not needed. This is why max-width will always be set to none

+ 93 - 0
services/web/frontend/js/features/ui/components/bootstrap-5/badge-link-with-tooltip.tsx

@@ -0,0 +1,93 @@
+import { OverlayTrigger, Tooltip } from 'react-bootstrap-5'
+import type { MergeAndOverride } from '../../../../../../types/utils'
+import BadgeLink, { type BadgeLinkProps } from './badge-link'
+import { useEffect, useRef, useState } from 'react'
+import classNames from 'classnames'
+
+type BadgeLinkWithTooltipProps = MergeAndOverride<
+  BadgeLinkProps,
+  {
+    placement?: 'top' | 'right' | 'bottom' | 'left'
+    tooltipTitle: string
+  }
+>
+
+function getElementWidth(el: Element) {
+  const elComputedStyle = window.getComputedStyle(el)
+  const elPaddingX =
+    parseFloat(elComputedStyle.paddingLeft) +
+    parseFloat(elComputedStyle.paddingRight)
+  const elBorderX =
+    parseFloat(elComputedStyle.borderLeftWidth) +
+    parseFloat(elComputedStyle.borderRightWidth)
+  return el.scrollWidth - elPaddingX - elBorderX
+}
+
+function BadgeLinkWithTooltip({
+  children,
+  tooltipTitle,
+  placement,
+  ...rest
+}: BadgeLinkWithTooltipProps) {
+  const badgeContentRef = useRef<HTMLElement>(null)
+  const [showTooltip, setShowTooltip] = useState(true)
+  const [noMaxWidth, setNoMaxWidth] = useState(false)
+
+  const badgeLinkClasses = classNames({ 'badge-link-no-max-width': noMaxWidth })
+
+  const renderTooltip = (props: any) => {
+    if (showTooltip) {
+      return (
+        <Tooltip
+          id={`badge-tooltip-${rest.href.replace(/\//g, '-')}`}
+          {...props}
+        >
+          {tooltipTitle}
+        </Tooltip>
+      )
+    } else {
+      return <></>
+    }
+  }
+
+  useEffect(() => {
+    if (badgeContentRef.current) {
+      // Check if tooltip needed.
+      // If .badge-content does not extend beyond max-width limit on
+      // .badge then tooltip is not needed. max-width is always
+      // removed when withTooltip exists and tooltip is not needed
+      // to avoid any differences in width calculation after font
+      // loaded (for example, Noto sans). Othwerise, badge might get
+      // clipped due to font loaded causing .badge-content to be
+      // greater than .badge max-width and no tooltip was determined
+      // to be needed with default font (for example, sans-serif)
+      const badgeContentWidth = badgeContentRef.current.scrollWidth
+      if (badgeContentRef.current?.parentElement) {
+        const badgeWidth = getElementWidth(
+          badgeContentRef.current?.parentElement
+        )
+        if (badgeContentWidth <= badgeWidth) {
+          // no tooltip and remove max-width
+          setNoMaxWidth(true)
+          setShowTooltip(false)
+        }
+      }
+    }
+  }, [])
+
+  return (
+    <OverlayTrigger placement={placement || 'bottom'} overlay={renderTooltip}>
+      <span>
+        <BadgeLink
+          {...rest}
+          badgeContentRef={badgeContentRef}
+          badgeLinkClasses={badgeLinkClasses}
+        >
+          {children}
+        </BadgeLink>
+      </span>
+    </OverlayTrigger>
+  )
+}
+
+export default BadgeLinkWithTooltip

+ 10 - 3
services/web/frontend/js/features/ui/components/bootstrap-5/badge-link.tsx

@@ -2,15 +2,22 @@ import classNames from 'classnames'
 import type { MergeAndOverride } from '../../../../../../types/utils'
 import Badge, { type BadgeProps } from './badge'
 
-type BadgeLinkProps = MergeAndOverride<
+export type BadgeLinkProps = MergeAndOverride<
   BadgeProps,
   {
     href: string
+    badgeContentRef?: React.RefObject<HTMLElement>
+    badgeLinkClasses?: string
   }
 >
 
-function BadgeLink({ href, children, ...badgeProps }: BadgeLinkProps) {
-  const containerClass = classNames('badge-link', {
+function BadgeLink({
+  href,
+  badgeLinkClasses,
+  children,
+  ...badgeProps
+}: BadgeLinkProps) {
+  const containerClass = classNames(badgeLinkClasses, 'badge-link', {
     [`badge-link-${badgeProps.bg}`]: badgeProps.bg,
   })
 

+ 5 - 2
services/web/frontend/js/features/ui/components/bootstrap-5/badge.tsx

@@ -5,14 +5,17 @@ export type BadgeProps = MergeAndOverride<
   BSBadgeProps,
   {
     prepend?: React.ReactNode
+    badgeContentRef?: React.RefObject<HTMLElement>
   }
 >
 
-function Badge({ prepend, children, ...rest }: BadgeProps) {
+function Badge({ prepend, children, badgeContentRef, ...rest }: BadgeProps) {
   return (
     <BSBadge {...rest}>
       {prepend && <span className="badge-prepend">{prepend}</span>}
-      <span className="badge-content">{children}</span>
+      <span className="badge-content" ref={badgeContentRef}>
+        {children}
+      </span>
     </BSBadge>
   )
 }

+ 6 - 0
services/web/frontend/stylesheets/bootstrap-5/components/badge.scss

@@ -105,6 +105,12 @@ $max-width: 160px;
     background-color: var(--neutral-30) !important;
   }
 
+  &.badge-link-no-max-width {
+    .badge {
+      max-width: none;
+    }
+  }
+
   .badge {
     @include body-sm;