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

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

GitOrigin-RevId: edcdcf8134698f17f607e003627a8b4123519b0b
This commit is contained in:
Jessica Lawshe 2024-11-15 10:31:24 -06:00 committed by Copybot
parent ade1b7f2bc
commit 5c63188200
5 changed files with 116 additions and 9 deletions

View file

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

View file

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

View file

@ -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,
})

View file

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

View file

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