fix(common): constrain variable tooltip to viewport for long values (#5878)

Co-authored-by: aviu16 <aviu16@users.noreply.github.com>
Co-authored-by: nivedin <nivedinp@gmail.com>
Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Eve 2026-02-23 13:03:14 -05:00 committed by GitHub
parent c687473889
commit 562d2919ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 658 additions and 29 deletions

View file

@ -16,6 +16,25 @@
@apply flex-col;
}
// Constrained tooltip styles to prevent overflow beyond the viewport
// when environment variable values are very long (fixes #5876)
.env-tooltip-constrained {
@apply flex-col;
@apply w-full;
@apply box-border;
@apply overflow-hidden;
.env-tooltip-value {
@apply break-words;
@apply break-all;
@apply whitespace-pre-wrap;
@apply overflow-hidden;
@apply inline-block;
@apply max-w-full;
@apply align-top;
}
}
.tippy-content {
@apply flex;
@apply text-tiny;

View file

@ -41,6 +41,10 @@ import {
ENV_VAR_NAME_REGEX,
HOPP_ENVIRONMENT_REGEX,
} from "~/helpers/environment-regex"
import {
constrainTooltipToViewport,
createTooltipValueRow,
} from "~/helpers/utils/tooltip"
const HOPP_ENV_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
@ -49,6 +53,8 @@ const HOPP_COLLECTION_ENVIRONMENT_HIGHLIGHT = "collection-variable-highlight"
const HOPP_ENVIRONMENT_HIGHLIGHT = "environment-variable-highlight"
const HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT = "global-variable-highlight"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "environment-not-found-highlight"
// Keep value rows above overlapping CodeMirror decoration layers inside tooltip content.
const TOOLTIP_ENV_CONTAINER_Z_INDEX_CLASS = "!z-[1002]"
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
@ -282,36 +288,31 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const envContainer = document.createElement("div")
tooltipContainer.appendChild(envContainer)
envContainer.className =
"flex flex-col items-start space-y-1 flex-1 w-full mt-2 !z-[1002]"
envContainer.className = `flex flex-col items-start space-y-1 flex-1 w-full mt-2 ${TOOLTIP_ENV_CONTAINER_Z_INDEX_CLASS}`
envContainer.style.overflow = "hidden"
const initialValueBlock = document.createElement("div")
initialValueBlock.className = "flex items-center space-x-2"
const initialValueTitle = document.createElement("div")
initialValueTitle.textContent = "Initial"
initialValueTitle.className = "font-bold mr-4 "
const initialValue = document.createElement("span")
initialValue.textContent = envInitialValue || ""
initialValueBlock.appendChild(initialValueTitle)
initialValueBlock.appendChild(initialValue)
// Use createTooltipValueRow for overflow-safe value display
const initialValueRow = createTooltipValueRow(
"Initial",
envInitialValue
)
const currentValueRow = createTooltipValueRow(
"Current",
envCurrentValue
)
const currentValueBlock = document.createElement("div")
currentValueBlock.className = "flex items-center space-x-2"
const currentValueTitle = document.createElement("div")
currentValueTitle.textContent = "Current"
currentValueTitle.className = "font-bold mr-1.5"
const currentValue = document.createElement("span")
currentValue.textContent = envCurrentValue || ""
currentValueBlock.appendChild(currentValueTitle)
currentValueBlock.appendChild(currentValue)
envContainer.appendChild(initialValueRow)
envContainer.appendChild(currentValueRow)
envContainer.appendChild(initialValueBlock)
envContainer.appendChild(currentValueBlock)
tooltipContainer.className = "tippy-content env-tooltip-content"
tooltipContainer.className =
"tippy-content env-tooltip-content env-tooltip-constrained"
dom.className = "tippy-box"
dom.dataset.theme = "tooltip"
dom.appendChild(tooltipContainer)
// Apply viewport-aware overflow constraints to the tooltip
constrainTooltipToViewport(dom, tooltipContainer)
return { dom }
},
}

View file

@ -9,6 +9,10 @@ import { HOPP_SUPPORTED_PREDEFINED_VARIABLES } from "@hoppscotch/data"
import IconSquareAsterisk from "~icons/lucide/square-asterisk?raw"
import { isComment } from "./helpers"
import {
constrainTooltipToViewport,
truncateText,
} from "~/helpers/utils/tooltip"
const HOPP_PREDEFINED_VARIABLES_REGEX = /(<<\$[a-zA-Z0-9-_]+>>)/g
@ -76,7 +80,7 @@ const cursorTooltipField = () =>
const variableIcon = `<span class="inline-flex items-center justify-center my-1">${IconSquareAsterisk}</span>`
const variableDescription =
variable !== undefined
? `${variableName} - ${variable.description}`
? `${variableName} - ${truncateText(variable.description)}`
: `${variableName} is not a valid predefined variable.`
return {
@ -114,14 +118,18 @@ const cursorTooltipField = () =>
tooltipContainer.appendChild(envContainer)
envContainer.className =
"flex flex-col items-start space-y-1 flex-1 w-full mt-2"
envContainer.style.overflow = "hidden"
const valueBlock = document.createElement("div")
valueBlock.className = "flex items-center space-x-2"
valueBlock.className = "flex items-start space-x-2"
valueBlock.style.width = "100%"
const valueTitle = document.createElement("div")
const value = document.createElement("span")
value.textContent = variableDescription || ""
value.className = "env-tooltip-value"
value.textContent = variableDescription
valueTitle.textContent = "Value"
valueTitle.className = "font-bold mr-4 "
valueTitle.className = "font-bold mr-4"
valueTitle.style.flexShrink = "0"
valueBlock.appendChild(valueTitle)
valueBlock.appendChild(value)
@ -130,9 +138,14 @@ const cursorTooltipField = () =>
dom.className = "tippy-box"
dom.dataset.theme = "tooltip"
tooltipContainer.className = "tippy-content env-tooltip-content"
tooltipContainer.className =
"tippy-content env-tooltip-content env-tooltip-constrained"
dom.appendChild(tooltipContainer)
// Apply viewport-aware overflow constraints
constrainTooltipToViewport(dom, tooltipContainer)
return { dom }
},
}

View file

@ -0,0 +1,399 @@
import { describe, expect, test, beforeEach, afterEach } from "vitest"
import {
truncateText,
formatTooltipValue,
calculateTooltipDimensions,
applyTooltipOverflowStyles,
createTooltipValueRow,
constrainTooltipToViewport,
TOOLTIP_MAX_VALUE_LENGTH,
TOOLTIP_MAX_HEIGHT_PX,
TOOLTIP_MAX_WIDTH_PX,
TOOLTIP_MIN_WIDTH_PX,
TOOLTIP_VIEWPORT_MARGIN_PX,
} from "../tooltip"
// ─── truncateText ────────────────────────────────────────────────
describe("truncateText", () => {
test("returns original string when within default max length", () => {
const short = "hello world"
expect(truncateText(short)).toBe(short)
})
test("returns original string when exactly at max length", () => {
const exact = "a".repeat(TOOLTIP_MAX_VALUE_LENGTH)
expect(truncateText(exact)).toBe(exact)
})
test("truncates string exceeding default max length and appends char count", () => {
const long = "x".repeat(TOOLTIP_MAX_VALUE_LENGTH + 50)
const result = truncateText(long)
expect(result.length).toBeLessThan(long.length)
expect(result).toContain("\u2026")
expect(result).toContain(`${long.length} chars)`)
})
test("truncates string with custom max length", () => {
const text = "abcdefghij"
const result = truncateText(text, 5)
expect(result).toBe("abcde\u2026 (truncated, 10 chars)")
})
test("returns empty string for null input", () => {
expect(truncateText(null as unknown as string)).toBe("")
})
test("returns empty string for undefined input", () => {
expect(truncateText(undefined as unknown as string)).toBe("")
})
test("returns empty string for empty string input", () => {
expect(truncateText("")).toBe("")
})
test("handles maxLength of 0 by truncating everything", () => {
const result = truncateText("abc", 0)
expect(result).toContain("\u2026")
expect(result).toContain("3 chars)")
})
test("handles maxLength of 1", () => {
const result = truncateText("abc", 1)
expect(result).toBe("a\u2026 (truncated, 3 chars)")
})
test("preserves unicode characters during truncation", () => {
const emoji = "\u{1F600}".repeat(10)
const result = truncateText(emoji, 3)
expect(result).toContain("\u2026")
})
test("handles very long strings (10000+ chars) without error", () => {
const veryLong = "z".repeat(10000)
const result = truncateText(veryLong, 100)
expect(result.startsWith("z".repeat(100))).toBe(true)
expect(result).toContain("10000 chars)")
})
})
// ─── formatTooltipValue ──────────────────────────────────────────
describe("formatTooltipValue", () => {
test("returns empty string for undefined", () => {
expect(formatTooltipValue(undefined)).toBe("")
})
test("returns empty string for null", () => {
expect(formatTooltipValue(null)).toBe("")
})
test("returns 'Empty' for empty string", () => {
expect(formatTooltipValue("")).toBe("Empty")
})
test("returns original value for short strings", () => {
expect(formatTooltipValue("my-value")).toBe("my-value")
})
test("truncates long values", () => {
const longValue = "v".repeat(600)
const result = formatTooltipValue(longValue)
expect(result).toContain("\u2026")
expect(result).toContain("600 chars)")
})
test("respects custom maxLength parameter", () => {
const result = formatTooltipValue("abcdef", 3)
expect(result).toBe("abc\u2026 (truncated, 6 chars)")
})
test("does not truncate when value equals maxLength", () => {
const result = formatTooltipValue("abc", 3)
expect(result).toBe("abc")
})
})
// ─── calculateTooltipDimensions ──────────────────────────────────
describe("calculateTooltipDimensions", () => {
test("returns max width capped at TOOLTIP_MAX_WIDTH_PX for large viewports", () => {
const { maxWidth } = calculateTooltipDimensions(1920, 1080)
expect(maxWidth).toBe(TOOLTIP_MAX_WIDTH_PX)
})
test("returns viewport-based width for small viewports", () => {
const viewportWidth = 300
const margin = TOOLTIP_VIEWPORT_MARGIN_PX
const { maxWidth } = calculateTooltipDimensions(viewportWidth, 600)
// effectiveWidth = 300 - 16*2 = 268, which is between MIN and MAX
expect(maxWidth).toBe(viewportWidth - margin * 2)
})
test("ensures minimum width for very small viewports", () => {
const { maxWidth } = calculateTooltipDimensions(100, 100)
expect(maxWidth).toBe(68)
})
test("respects custom margin", () => {
const customMargin = 50
const { maxWidth } = calculateTooltipDimensions(400, 600, customMargin)
// effectiveWidth = 400 - 100 = 300
expect(maxWidth).toBe(300)
})
test("caps maxHeight at TOOLTIP_MAX_HEIGHT_PX", () => {
const { maxHeight } = calculateTooltipDimensions(1920, 1080)
expect(maxHeight).toBe(TOOLTIP_MAX_HEIGHT_PX)
})
test("uses available height when viewport is small", () => {
const { maxHeight } = calculateTooltipDimensions(1920, 200)
// effectiveHeight = 200 - 32 = 168
expect(maxHeight).toBe(168)
})
test("handles zero dimensions gracefully", () => {
const dims = calculateTooltipDimensions(0, 0)
expect(dims.maxWidth).toBe(0)
expect(dims.maxHeight).toBeLessThanOrEqual(300)
})
})
// ─── applyTooltipOverflowStyles ──────────────────────────────────
describe("applyTooltipOverflowStyles", () => {
let element: HTMLElement
beforeEach(() => {
element = document.createElement("div")
})
test("sets maxWidth to default TOOLTIP_MAX_WIDTH_PX when not specified", () => {
applyTooltipOverflowStyles(element)
expect(element.style.maxWidth).toBe(`${TOOLTIP_MAX_WIDTH_PX}px`)
})
test("sets maxWidth to custom value", () => {
applyTooltipOverflowStyles(element, 300)
expect(element.style.maxWidth).toBe("300px")
})
test("sets minWidth to TOOLTIP_MIN_WIDTH_PX when maxWidth allows", () => {
applyTooltipOverflowStyles(element)
expect(element.style.minWidth).toBe(`${TOOLTIP_MIN_WIDTH_PX}px`)
})
test("clamps minWidth to maxWidth on narrow viewports", () => {
applyTooltipOverflowStyles(element, 68)
expect(element.style.minWidth).toBe("68px")
})
test("sets boxSizing to border-box", () => {
applyTooltipOverflowStyles(element)
expect(element.style.boxSizing).toBe("border-box")
})
test("sets maxHeight when provided", () => {
applyTooltipOverflowStyles(element, undefined, 250)
expect(element.style.maxHeight).toBe("250px")
})
test("does not set maxHeight when undefined", () => {
applyTooltipOverflowStyles(element)
expect(element.style.maxHeight).toBe("")
})
})
// ─── createTooltipValueRow ───────────────────────────────────────
describe("createTooltipValueRow", () => {
test("creates a div element", () => {
const row = createTooltipValueRow("Initial", "test-value")
expect(row.tagName).toBe("DIV")
})
test("contains a label element with correct text", () => {
const row = createTooltipValueRow("Initial", "test-value")
const label = row.querySelector("div")
expect(label).not.toBeNull()
expect(label!.textContent).toBe("Initial")
})
test("contains a value element with correct text", () => {
const row = createTooltipValueRow("Current", "my-value")
const value = row.querySelector("span")
expect(value).not.toBeNull()
expect(value!.textContent).toBe("my-value")
})
test("truncates long values", () => {
const longValue = "a".repeat(600)
const row = createTooltipValueRow("Initial", longValue)
const value = row.querySelector("span")
expect(value!.textContent).toContain("\u2026")
expect(value!.textContent).toContain("600 chars)")
})
test("shows 'Empty' for empty string value", () => {
const row = createTooltipValueRow("Current", "")
const value = row.querySelector("span")
expect(value!.textContent).toBe("Empty")
})
test("shows empty string for undefined value", () => {
const row = createTooltipValueRow("Initial", undefined)
const value = row.querySelector("span")
expect(value!.textContent).toBe("")
})
test("shows empty string for null value", () => {
const row = createTooltipValueRow("Current", null)
const value = row.querySelector("span")
expect(value!.textContent).toBe("")
})
test("applies env-tooltip-value class to the value element", () => {
const row = createTooltipValueRow("Initial", "test")
const value = row.querySelector(".env-tooltip-value")
expect(value).not.toBeNull()
})
test("sets title attribute when value is truncated", () => {
const longValue = "b".repeat(600)
const row = createTooltipValueRow("Initial", longValue, 100)
const value = row.querySelector("span")
expect(value!.title).toContain("600 characters")
})
test("does not set title attribute when value is not truncated", () => {
const row = createTooltipValueRow("Initial", "short")
const value = row.querySelector("span")
expect(value!.title).toBe("")
})
test("label has flexShrink 0 so it does not compress", () => {
const row = createTooltipValueRow("Initial", "value")
const label = row.querySelector("div")
expect(label!.style.flexShrink).toBe("0")
})
test("row has width 100%", () => {
const row = createTooltipValueRow("Current", "value")
expect(row.style.width).toBe("100%")
})
test("respects custom maxValueLength parameter", () => {
const result = createTooltipValueRow("Initial", "abcdefghij", 5)
const value = result.querySelector("span")
expect(value!.textContent).toContain("\u2026")
})
test("applies env-tooltip-value class for overflow styles", () => {
const row = createTooltipValueRow("Initial", "test")
const value = row.querySelector("span")
expect(value!.classList.contains("env-tooltip-value")).toBe(true)
})
})
// ─── constrainTooltipToViewport ──────────────────────────────────
describe("constrainTooltipToViewport", () => {
let tooltipBox: HTMLElement
let tooltipContent: HTMLElement
let originalInnerWidth: number
let originalInnerHeight: number
beforeEach(() => {
tooltipBox = document.createElement("div")
tooltipContent = document.createElement("div")
originalInnerWidth = window.innerWidth
originalInnerHeight = window.innerHeight
// jsdom has window.innerWidth and innerHeight set to 0 by default
// We mock them for consistent test results
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1024,
})
Object.defineProperty(window, "innerHeight", {
writable: true,
configurable: true,
value: 768,
})
})
afterEach(() => {
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: originalInnerWidth,
})
Object.defineProperty(window, "innerHeight", {
writable: true,
configurable: true,
value: originalInnerHeight,
})
})
test("sets maxWidth on tooltipBox", () => {
constrainTooltipToViewport(tooltipBox, tooltipContent)
expect(tooltipBox.style.maxWidth).not.toBe("")
})
test("sets minWidth on tooltipBox", () => {
constrainTooltipToViewport(tooltipBox, tooltipContent)
expect(tooltipBox.style.minWidth).toBe(`${TOOLTIP_MIN_WIDTH_PX}px`)
})
test("sets maxWidth 100% on tooltipContent", () => {
constrainTooltipToViewport(tooltipBox, tooltipContent)
expect(tooltipContent.style.maxWidth).toBe("100%")
})
test("sets overflow hidden on tooltipContent", () => {
constrainTooltipToViewport(tooltipBox, tooltipContent)
expect(tooltipContent.style.overflow).toBe("hidden")
})
test("sets boxSizing on tooltipContent", () => {
constrainTooltipToViewport(tooltipBox, tooltipContent)
expect(tooltipContent.style.boxSizing).toBe("border-box")
})
test("works with small viewport", () => {
Object.defineProperty(window, "innerWidth", { value: 250 })
Object.defineProperty(window, "innerHeight", { value: 200 })
constrainTooltipToViewport(tooltipBox, tooltipContent)
// Should use minimum width since viewport is too small
expect(parseInt(tooltipBox.style.maxWidth)).toBeLessThanOrEqual(
TOOLTIP_MAX_WIDTH_PX
)
})
})
// ─── Constants ───────────────────────────────────────────────────
describe("Tooltip Constants", () => {
test("TOOLTIP_MAX_VALUE_LENGTH is a positive number", () => {
expect(TOOLTIP_MAX_VALUE_LENGTH).toBeGreaterThan(0)
})
test("TOOLTIP_MAX_WIDTH_PX is a positive number", () => {
expect(TOOLTIP_MAX_WIDTH_PX).toBeGreaterThan(0)
})
test("TOOLTIP_MIN_WIDTH_PX is less than TOOLTIP_MAX_WIDTH_PX", () => {
expect(TOOLTIP_MIN_WIDTH_PX).toBeLessThan(TOOLTIP_MAX_WIDTH_PX)
})
test("TOOLTIP_MIN_WIDTH_PX is a positive number", () => {
expect(TOOLTIP_MIN_WIDTH_PX).toBeGreaterThan(0)
})
test("TOOLTIP_VIEWPORT_MARGIN_PX is a positive number", () => {
expect(TOOLTIP_VIEWPORT_MARGIN_PX).toBeGreaterThan(0)
})
})

View file

@ -0,0 +1,197 @@
/**
* Utility functions for tooltip text truncation, formatting, and
* viewport-constrained positioning. These helpers ensure that long
* variable values displayed in editor hover tooltips stay within
* the visible viewport boundaries.
*
* @module tooltip
*/
/** Default maximum character length before a tooltip value is truncated. */
export const TOOLTIP_MAX_VALUE_LENGTH = 500
/** Default maximum pixel width for the tooltip container. */
export const TOOLTIP_MAX_WIDTH_PX = 450
/** Minimum pixel width for the tooltip container. */
export const TOOLTIP_MIN_WIDTH_PX = 200
/** Maximum pixel height for the tooltip container. */
export const TOOLTIP_MAX_HEIGHT_PX = 300
/** Viewport margin in pixels — the tooltip will stay this far from edges. */
export const TOOLTIP_VIEWPORT_MARGIN_PX = 16
/**
* Truncates a string to a maximum length and appends an ellipsis
* indicator when the text exceeds the limit.
*
* @param text - The input string to potentially truncate
* @param maxLength - The maximum number of characters to keep
* (defaults to TOOLTIP_MAX_VALUE_LENGTH)
* @returns The original string if within limits, or a truncated version
* with a trailing ellipsis and character-count annotation
*
* @example
* ```ts
* truncateText("short") // "short"
* truncateText("a".repeat(600)) // "aaa...aaa (600 chars)"
* truncateText("abc", 2) // "ab... (3 chars)"
* ```
*/
export function truncateText(
text: string,
maxLength: number = TOOLTIP_MAX_VALUE_LENGTH
): string {
if (!text) {
return text ?? ""
}
const codePoints = Array.from(text)
if (codePoints.length <= maxLength) return text
// Show the beginning of the text plus a count so the user knows
// the full length of the value.
const truncated = codePoints.slice(0, maxLength).join("")
return `${truncated}\u2026 (truncated, ${codePoints.length} chars)`
}
/**
* Formats a variable value for display inside a tooltip.
* Handles null/undefined/empty cases and applies truncation.
*
* @param value - The raw variable value
* @param maxLength - Optional max character length
* @returns A human-readable display string
*/
export function formatTooltipValue(
value: string | undefined | null,
maxLength: number = TOOLTIP_MAX_VALUE_LENGTH
): string {
if (value === undefined || value === null) {
return ""
}
if (value === "") {
return "Empty"
}
return truncateText(value, maxLength)
}
/**
* Calculates appropriate tooltip dimensions so the tooltip stays
* within the visible viewport.
*
* @param viewportWidth - Current viewport width in pixels
* @param viewportHeight - Current viewport height in pixels
* @param margin - Margin from viewport edges (defaults to TOOLTIP_VIEWPORT_MARGIN_PX)
* @returns An object with maxWidth and maxHeight constraints in pixels
*/
export function calculateTooltipDimensions(
viewportWidth: number,
viewportHeight: number,
margin: number = TOOLTIP_VIEWPORT_MARGIN_PX
): { maxWidth: number; maxHeight: number } {
const effectiveWidth = Math.max(viewportWidth - margin * 2, 0)
const effectiveHeight = Math.max(viewportHeight - margin * 2, 0)
const maxWidth = Math.min(effectiveWidth, TOOLTIP_MAX_WIDTH_PX)
const maxHeight = Math.min(effectiveHeight, TOOLTIP_MAX_HEIGHT_PX)
return { maxWidth, maxHeight }
}
/**
* Applies overflow-safe styles to a tooltip DOM element so that its
* content is constrained to the viewport. This mutates the element
* in-place by setting inline styles.
*
* @param element - The tooltip DOM element to constrain
* @param maxWidth - Maximum width in pixels (or undefined to use default)
* @param maxHeight - Maximum height in pixels (or undefined to skip)
*/
export function applyTooltipOverflowStyles(
element: HTMLElement,
maxWidth?: number,
maxHeight?: number
): void {
const resolvedMaxWidth = maxWidth ?? TOOLTIP_MAX_WIDTH_PX
const resolvedMinWidth = Math.min(TOOLTIP_MIN_WIDTH_PX, resolvedMaxWidth)
element.style.maxWidth = `${resolvedMaxWidth}px`
element.style.minWidth = `${resolvedMinWidth}px`
element.style.boxSizing = "border-box"
if (maxHeight !== undefined) {
element.style.maxHeight = `${maxHeight}px`
}
}
/**
* Creates a fully styled value row for the tooltip, including
* a label (e.g., "Initial" or "Current") and a value element
* with proper overflow handling.
*
* @param label - The label text (e.g., "Initial", "Current", "Value")
* @param value - The raw value string to display
* @param maxValueLength - Maximum characters before truncation
* @returns The row container element
*/
export function createTooltipValueRow(
label: string,
value: string | undefined | null,
maxValueLength: number = TOOLTIP_MAX_VALUE_LENGTH
): HTMLDivElement {
const row = document.createElement("div")
row.className = "flex items-start space-x-2"
row.style.width = "100%"
const labelEl = document.createElement("div")
labelEl.textContent = label
labelEl.className = "font-bold"
labelEl.style.flexShrink = "0"
labelEl.style.minWidth = "50px"
labelEl.style.marginRight = "0.5rem"
const valueEl = document.createElement("span")
valueEl.className = "env-tooltip-value"
const displayValue = formatTooltipValue(value, maxValueLength)
valueEl.textContent = displayValue
// Add a title attribute with the truncated indicator so users
// know the full length if it was truncated
if (value && value.length > maxValueLength) {
valueEl.title = `Full value: ${value.length} characters`
}
row.appendChild(labelEl)
row.appendChild(valueEl)
return row
}
/**
* Applies overflow constraints to the outer tooltip container element
* (the `.tippy-box` wrapper). This ensures the entire tooltip
* respects viewport boundaries.
*
* @param tooltipBox - The outer `.tippy-box` element
* @param tooltipContent - The inner `.tippy-content` element
*/
export function constrainTooltipToViewport(
tooltipBox: HTMLElement,
tooltipContent: HTMLElement
): void {
const { maxWidth, maxHeight } = calculateTooltipDimensions(
window.innerWidth,
window.innerHeight
)
applyTooltipOverflowStyles(tooltipBox, maxWidth, maxHeight)
tooltipContent.style.maxWidth = "100%"
tooltipContent.style.overflow = "hidden"
tooltipContent.style.boxSizing = "border-box"
}