UI: Improve TooltipTruncatedText and now underlying useCheckTruncatedElement (#29232)

## For #27667 

- Have `TooltipTruncatedText` component use `useCheckTruncatedElement`
to track its current state of truncation.
- Update `useCheckTruncatedElement` to re-evaluate truncation state
based on changes to the width of
the element itself as opposed to changes to viewport width. This
facilitates truncation when the
width of the element is updated due to user interaction / change in UI
state other than window resize, e.g. checking a policy in the policy
software automations modal (see issue description for details
reproduction instructions there).

**Truncation with tooltip successful for UI state changes:**

![ezgif-27969fb4a17d3e](https://github.com/user-attachments/assets/66156bd5-7948-4e73-9c11-4a08fde44189)

Truncation with tooltip successful for viewport resizing:

![ezgif-2515f715b05436](https://github.com/user-attachments/assets/8ef70579-1e89-4a4b-9fd0-93a4776d3151)

![ezgif-24a953c65500d9](https://github.com/user-attachments/assets/fc2f302b-6f7e-463e-97d2-9978b97c5601)

- [x] Changes file added for user-visible changes in `changes/⁄
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
jacobshandling
2025-05-20 15:41:53 -07:00
committed by GitHub
co-authored by Jacob Shandling
parent 28ba274f1f
commit 19fe0ff5ce
4 changed files with 27 additions and 24 deletions
+1
View File
@@ -0,0 +1 @@
- Improve effectiveness of app-wide text-truncation-into-tooltip functionality.
@@ -1,9 +1,10 @@
import React, { useState, useRef, useLayoutEffect } from "react";
import React, { useRef } from "react";
import { uniqueId } from "lodash";
import classnames from "classnames";
import ReactTooltip from "react-tooltip";
import { COLORS } from "styles/var/colors";
import { useCheckTruncatedElement } from "hooks/useCheckTruncatedElement";
interface ITooltipTruncatedTextCellProps {
value: React.ReactNode;
@@ -30,22 +31,15 @@ const TooltipTruncatedText = ({
// Tooltip visibility logic: Enable only when text is truncated
const ref = useRef<HTMLInputElement>(null);
const [tooltipDisabled, setTooltipDisabled] = useState(true);
useLayoutEffect(() => {
if (ref?.current !== null) {
const scrollWidth = ref.current.scrollWidth;
const offsetWidth = ref.current.offsetWidth;
setTooltipDisabled(scrollWidth <= offsetWidth);
}
}, [ref]);
// End
const isTruncated = useCheckTruncatedElement(ref);
const tooltipId = uniqueId();
return (
<div ref={ref} className={classNames}>
<div className={classNames}>
<div className="tooltip-truncated" data-tip data-for={tooltipId}>
<span className={tooltipDisabled ? "" : "truncated"}>{value}</span>
<div ref={ref} className={isTruncated ? "truncated" : undefined}>
{value}
</div>
</div>
<ReactTooltip
place="top"
@@ -56,7 +50,7 @@ const TooltipTruncatedText = ({
className="truncated-tooltip" // responsive widths
clickable
delayHide={200} // need delay set to hover using clickable
disable={tooltipDisabled}
disable={!isTruncated}
>
<>
{tooltip ?? value}
@@ -11,8 +11,6 @@
}
.truncated {
display: inline-block;
max-width: 99%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
+18 -8
View File
@@ -9,17 +9,27 @@ export const useCheckTruncatedElement = <T extends HTMLElement>(
) => {
const [isTruncated, setIsTruncated] = useState(false);
const updateIsTruncated = (element: HTMLElement) => {
const { scrollWidth, clientWidth } = element;
setIsTruncated(scrollWidth > clientWidth);
};
useLayoutEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
updateIsTruncated(entry.target as HTMLElement);
});
});
const element = ref.current;
function updateIsTruncated() {
if (element) {
const { scrollWidth, clientWidth } = element;
setIsTruncated(scrollWidth > clientWidth);
}
if (element) {
updateIsTruncated(element);
resizeObserver.observe(ref.current as HTMLElement);
}
window.addEventListener("resize", updateIsTruncated);
updateIsTruncated();
return () => window.removeEventListener("resize", updateIsTruncated);
return () => {
if (element) {
resizeObserver.unobserve(element);
}
};
}, [ref]);
return isTruncated;