diff --git a/src/visualBuilder/generators/generateOverlay.tsx b/src/visualBuilder/generators/generateOverlay.tsx index e09e9be9..35915ffb 100644 --- a/src/visualBuilder/generators/generateOverlay.tsx +++ b/src/visualBuilder/generators/generateOverlay.tsx @@ -10,6 +10,10 @@ import { FieldDataType } from "../utils/types/index.types"; import { getFieldType } from "../utils/getFieldType"; import { getMultilinePlaintext } from "../utils/getMultilinePlaintext"; import { showAllHiddenHighlightedCommentIcons } from "./generateHighlightedComment"; +import { + restoreTruncateStyles, + hasStoredLineClampStyles, +} from "../utils/truncateHandler"; /** * Adds a focus overlay to the target element. @@ -224,6 +228,18 @@ export function hideOverlay(params: HideOverlayParams): void { VisualBuilder.VisualBuilderGlobalState.value.focusElementObserver = null; } + + // Restore truncate styles for the previously selected element before hiding overlay + const previousSelectedElement = + VisualBuilder.VisualBuilderGlobalState.value + .previousSelectedEditableDOM; + if ( + previousSelectedElement && + hasStoredLineClampStyles(previousSelectedElement) + ) { + restoreTruncateStyles(previousSelectedElement); + } + hideFocusOverlay({ visualBuilderContainer: params.visualBuilderContainer, visualBuilderOverlayWrapper: params.visualBuilderOverlayWrapper, diff --git a/src/visualBuilder/listeners/mouseClick.ts b/src/visualBuilder/listeners/mouseClick.ts index d65ed0b3..a16db704 100644 --- a/src/visualBuilder/listeners/mouseClick.ts +++ b/src/visualBuilder/listeners/mouseClick.ts @@ -32,6 +32,11 @@ import { fixSvgXPath } from "../utils/collabUtils"; import { v4 as uuidV4 } from "uuid"; import { CslpData } from "../../cslp/types/cslp.types"; import { fetchEntryPermissionsAndStageDetails } from "../utils/fetchEntryPermissionsAndStageDetails"; +import { + removeTruncateStyles, + restoreTruncateStyles, + hasStoredLineClampStyles, +} from "../utils/truncateHandler"; export type HandleBuilderInteractionParams = Omit< EventListenerHandlerParams, @@ -243,6 +248,14 @@ function cleanResidualsIfNeeded( previousSelectedElement !== editableElement) || params.reEvaluate ) { + // Restore truncate styles for the previously selected element + if ( + previousSelectedElement && + hasStoredLineClampStyles(previousSelectedElement) + ) { + restoreTruncateStyles(previousSelectedElement); + } + cleanIndividualFieldResidual({ overlayWrapper: params.overlayWrapper!, visualBuilderContainer: params.visualBuilderContainer, @@ -277,6 +290,10 @@ function addOverlayAndToolbar( isVariant: boolean ) { VisualBuilder.VisualBuilderGlobalState.value.isFocussed = true; + + // Remove truncate styles from the focused element + removeTruncateStyles(editableElement); + addOverlay({ overlayWrapper: params.overlayWrapper, resizeObserver: params.resizeObserver, diff --git a/src/visualBuilder/utils/__test__/clearStyles.test.ts b/src/visualBuilder/utils/__test__/clearStyles.test.ts new file mode 100644 index 00000000..80a22fac --- /dev/null +++ b/src/visualBuilder/utils/__test__/clearStyles.test.ts @@ -0,0 +1,237 @@ +import { clearVisibilityStyles, restoreVisibilityStyles } from "../clearStyles"; + +describe("clearStyles", () => { + let element: HTMLDivElement; + + beforeEach(() => { + element = document.createElement("div"); + document.body.appendChild(element); + }); + + afterEach(() => { + document.body.removeChild(element); + }); + + describe("clearVisibilityStyles", () => { + it("should hide element and disable transitions/animations", () => { + // Set initial styles + element.style.visibility = "visible"; + element.style.transition = "all 0.3s ease"; + element.style.animation = "fadeIn 1s"; + + clearVisibilityStyles(element); + + expect(element.style.visibility).toBe("hidden"); + expect(element.style.transition).toBe("none"); + expect(element.style.animation).toBe("none"); + }); + + it("should handle element with no initial styles", () => { + clearVisibilityStyles(element); + + expect(element.style.visibility).toBe("hidden"); + expect(element.style.transition).toBe("none"); + expect(element.style.animation).toBe("none"); + }); + + it("should store original style values", () => { + // Set initial styles + element.style.visibility = "visible"; + element.style.transition = "opacity 0.5s"; + element.style.animation = "slideIn 2s"; + + clearVisibilityStyles(element); + + // Verify styles are cleared + expect(element.style.visibility).toBe("hidden"); + expect(element.style.transition).toBe("none"); + expect(element.style.animation).toBe("none"); + + // Verify we can restore (this indirectly tests that values were stored) + restoreVisibilityStyles(element); + expect(element.style.visibility).toBe("visible"); + expect(element.style.transition).toBe("opacity 0.5s"); + expect(element.style.animation).toBe("slideIn 2s"); + }); + + it("should handle multiple calls on the same element", () => { + element.style.visibility = "visible"; + element.style.transition = "all 0.3s"; + + // First call + clearVisibilityStyles(element); + expect(element.style.visibility).toBe("hidden"); + + // Second call should not overwrite stored values + element.style.visibility = "hidden"; // Simulate current state + clearVisibilityStyles(element); + + // Restore should still work with original values + restoreVisibilityStyles(element); + expect(element.style.visibility).toBe("visible"); + expect(element.style.transition).toBe("all 0.3s"); + }); + + it("should handle empty string style values", () => { + element.style.visibility = ""; + element.style.transition = ""; + element.style.animation = ""; + + clearVisibilityStyles(element); + restoreVisibilityStyles(element); + + expect(element.style.visibility).toBe(""); + expect(element.style.transition).toBe(""); + expect(element.style.animation).toBe(""); + }); + }); + + describe("restoreVisibilityStyles", () => { + it("should restore original styles after clearing", () => { + const originalVisibility = "visible"; + const originalTransition = "all 0.3s ease-in-out"; + const originalAnimation = "bounce 1s infinite"; + + element.style.visibility = originalVisibility; + element.style.transition = originalTransition; + element.style.animation = originalAnimation; + + clearVisibilityStyles(element); + restoreVisibilityStyles(element); + + expect(element.style.visibility).toBe(originalVisibility); + expect(element.style.transition).toBe(originalTransition); + expect(element.style.animation).toBe(originalAnimation); + }); + + it("should handle restore without prior clear (no stored values)", () => { + element.style.visibility = "visible"; + element.style.transition = "all 0.3s"; + + // Call restore without clearing first + restoreVisibilityStyles(element); + + // Should not modify styles when no stored values exist + expect(element.style.visibility).toBe("visible"); + expect(element.style.transition).toBe("all 0.3s"); + }); + + it("should remove stored values after restoration", () => { + element.style.visibility = "visible"; + clearVisibilityStyles(element); + restoreVisibilityStyles(element); + + // Second restore call should not affect styles + element.style.visibility = "hidden"; + restoreVisibilityStyles(element); + expect(element.style.visibility).toBe("hidden"); // Should remain unchanged + }); + + it("should restore empty string values correctly", () => { + // Start with empty styles (which is the default) + clearVisibilityStyles(element); + restoreVisibilityStyles(element); + + expect(element.style.visibility).toBe(""); + expect(element.style.transition).toBe(""); + expect(element.style.animation).toBe(""); + }); + + it("should handle partial style restoration", () => { + element.style.visibility = "visible"; + element.style.transition = "opacity 0.5s"; + // animation is not set (empty) + + clearVisibilityStyles(element); + restoreVisibilityStyles(element); + + expect(element.style.visibility).toBe("visible"); + expect(element.style.transition).toBe("opacity 0.5s"); + expect(element.style.animation).toBe(""); + }); + }); + + describe("WeakMap storage behavior", () => { + it("should handle multiple elements independently", () => { + const element1 = document.createElement("div"); + const element2 = document.createElement("div"); + document.body.appendChild(element1); + document.body.appendChild(element2); + + element1.style.visibility = "visible"; + element1.style.transition = "all 0.3s"; + element2.style.visibility = "hidden"; + element2.style.animation = "fadeIn 1s"; + + clearVisibilityStyles(element1); + clearVisibilityStyles(element2); + + restoreVisibilityStyles(element1); + restoreVisibilityStyles(element2); + + expect(element1.style.visibility).toBe("visible"); + expect(element1.style.transition).toBe("all 0.3s"); + expect(element1.style.animation).toBe(""); // Was not set originally + + expect(element2.style.visibility).toBe("hidden"); + expect(element2.style.transition).toBe(""); // Was not set originally + expect(element2.style.animation).toBe("fadeIn 1s"); + + document.body.removeChild(element1); + document.body.removeChild(element2); + }); + + it("should not leak memory after element removal", () => { + const tempElement = document.createElement("div"); + tempElement.style.visibility = "visible"; + + clearVisibilityStyles(tempElement); + + // Element is not in DOM and has no references + // WeakMap should allow garbage collection + // This test mainly documents the expected behavior + expect(tempElement.style.visibility).toBe("hidden"); + }); + }); + + describe("CSS property handling edge cases", () => { + it("should handle complex transition values", () => { + element.style.transition = + "opacity 0.3s ease-in-out, transform 0.5s linear"; + + clearVisibilityStyles(element); + expect(element.style.transition).toBe("none"); + + restoreVisibilityStyles(element); + expect(element.style.transition).toBe( + "opacity 0.3s ease-in-out, transform 0.5s linear" + ); + }); + + it("should handle complex animation values", () => { + element.style.animation = + "slideIn 0.5s ease-out, fadeIn 1s linear infinite"; + + clearVisibilityStyles(element); + expect(element.style.animation).toBe("none"); + + restoreVisibilityStyles(element); + expect(element.style.animation).toBe( + "slideIn 0.5s ease-out, fadeIn 1s linear infinite" + ); + }); + + it("should handle inherit and initial values", () => { + element.style.visibility = "inherit"; + element.style.transition = "initial"; + element.style.animation = "inherit"; + + clearVisibilityStyles(element); + restoreVisibilityStyles(element); + + expect(element.style.visibility).toBe("inherit"); + expect(element.style.transition).toBe("initial"); + expect(element.style.animation).toBe("inherit"); + }); + }); +}); diff --git a/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts b/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts index c8565a53..15181871 100644 --- a/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts +++ b/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts @@ -12,6 +12,7 @@ import { handleFieldInput, handleFieldKeyDown } from "../handleFieldMouseDown"; import { pasteAsPlainText } from "../pasteAsPlainText"; import { FieldDataType } from "../types/index.types"; import { updateFocussedState as updateFocussedStateActual } from "../updateFocussedState"; +import { clearVisibilityStyles } from "../clearStyles"; const isEllipsisActive = vi.mocked(isEllipsisActiveActual); const generatePseudoEditableElement = vi.mocked( @@ -67,6 +68,11 @@ vi.mock("../pasteAsPlainText", () => ({ pasteAsPlainText: vi.fn(), })); +vi.mock("../clearStyles", () => ({ + clearVisibilityStyles: vi.fn(), + restoreVisibilityStyles: vi.fn(), +})); + describe("enableInlineEditing", () => { let editableElement: HTMLDivElement; let mockedEditableElement: MockedObject; @@ -165,7 +171,7 @@ describe("enableInlineEditing", () => { }); expect(generatePseudoEditableElement).toHaveBeenCalled(); - expect(editableElement.style.visibility).toBe("hidden"); + expect(clearVisibilityStyles).toHaveBeenCalledWith(editableElement); expect(resizeObserver.observe).toHaveBeenCalled(); }); @@ -184,7 +190,7 @@ describe("enableInlineEditing", () => { }); expect(generatePseudoEditableElement).toHaveBeenCalled(); - expect(editableElement.style.visibility).toBe("hidden"); + expect(clearVisibilityStyles).toHaveBeenCalledWith(editableElement); }); it("should set field type attribute on pseudo element", () => { @@ -294,9 +300,9 @@ describe("enableInlineEditing", () => { }); expect(generatePseudoEditableElement).toHaveBeenCalled(); - expect(editableElement.style.visibility).toBe("hidden"); + expect(clearVisibilityStyles).toHaveBeenCalledWith(editableElement); expect(resizeObserver.observe).toHaveBeenCalled(); - + document.body.removeChild(editableElement); }); @@ -312,7 +318,9 @@ describe("enableInlineEditing", () => { }, }); - expect(editableElement.getAttribute("data-cs-last-edited")).toBe("true"); + expect(editableElement.getAttribute("data-cs-last-edited")).toBe( + "true" + ); }); it("should create pseudo element when field is last edited even with same content", () => { @@ -333,8 +341,8 @@ describe("enableInlineEditing", () => { }); expect(generatePseudoEditableElement).toHaveBeenCalled(); - expect(editableElement.style.visibility).toBe("hidden"); - + expect(clearVisibilityStyles).toHaveBeenCalledWith(editableElement); + document.body.removeChild(editableElement); }); @@ -358,8 +366,10 @@ describe("enableInlineEditing", () => { }); expect(generatePseudoEditableElement).not.toHaveBeenCalled(); - expect(editableElement.style.visibility).toBe(""); - expect(editableElement.getAttribute("data-cs-last-edited")).toBe("true"); + expect(clearVisibilityStyles).not.toHaveBeenCalled(); + expect(editableElement.getAttribute("data-cs-last-edited")).toBe( + "true" + ); document.body.removeChild(otherElement); }); @@ -380,13 +390,56 @@ describe("enableInlineEditing", () => { }); expect(generatePseudoEditableElement).toHaveBeenCalled(); - expect(editableElement.style.visibility).toBe("hidden"); - - const pseudoElement = visualBuilderContainer.querySelector(".visual-builder__pseudo-editable-element"); + expect(clearVisibilityStyles).toHaveBeenCalledWith(editableElement); + + const pseudoElement = visualBuilderContainer.querySelector( + ".visual-builder__pseudo-editable-element" + ); expect(pseudoElement).toBeTruthy(); expect(pseudoElement?.getAttribute("data-cslp")).toBeNull(); - expect(editableElement.getAttribute("data-cs-last-edited")).toBe("true"); - + expect(editableElement.getAttribute("data-cs-last-edited")).toBe( + "true" + ); + document.body.removeChild(editableElement); }); + + it("should call clearVisibilityStyles to immediately hide element when creating pseudo element", () => { + // Test that clearVisibilityStyles is called specifically for hiding transitions/animations + enableInlineEditing({ + expectedFieldData: "Different content", + editableElement, + fieldType: FieldDataType.SINGLELINE, + elements: { + visualBuilderContainer, + resizeObserver, + lastEditedField: null, + }, + }); + + // Verify clearVisibilityStyles was called with the correct element + expect(clearVisibilityStyles).toHaveBeenCalledTimes(1); + expect(clearVisibilityStyles).toHaveBeenCalledWith(editableElement); + + // Verify it was called before other operations + expect(generatePseudoEditableElement).toHaveBeenCalled(); + expect(resizeObserver.observe).toHaveBeenCalled(); + }); + + it("should not call clearVisibilityStyles when pseudo element is not needed", () => { + // When content matches and no ellipsis, no pseudo element should be created + enableInlineEditing({ + expectedFieldData: "Test content", + editableElement, + fieldType: FieldDataType.SINGLELINE, + elements: { + visualBuilderContainer, + resizeObserver, + lastEditedField: null, + }, + }); + + expect(clearVisibilityStyles).not.toHaveBeenCalled(); + expect(generatePseudoEditableElement).not.toHaveBeenCalled(); + }); }); diff --git a/src/visualBuilder/utils/clearStyles.ts b/src/visualBuilder/utils/clearStyles.ts new file mode 100644 index 00000000..12c4a908 --- /dev/null +++ b/src/visualBuilder/utils/clearStyles.ts @@ -0,0 +1,42 @@ +const elementStyles = new WeakMap< + HTMLElement, + { + visibility?: string; + transition?: string; + animation?: string; + } +>(); +/** + * Clears the visibility, transition, and animation styles of an element and stores the original values + * @param element - The element to clear the styles from + * VB-277 Fix: Clear visibility, transition, and animation styles of an element + */ + +export function clearVisibilityStyles(element: HTMLElement) { + // Only store original values if not already stored + if (!elementStyles.has(element)) { + const originalStyleValues = { + visibility: element.style.visibility, + transition: element.style.transition, + animation: element.style.animation, + }; + + elementStyles.set(element, originalStyleValues); + } + + element.style.visibility = "hidden"; + element.style.transition = "none"; + element.style.animation = "none"; +} + +export function restoreVisibilityStyles(element: HTMLElement) { + const storedStyles = elementStyles.get(element); + + if (storedStyles) { + element.style.visibility = storedStyles.visibility || ""; + element.style.transition = storedStyles.transition || ""; + element.style.animation = storedStyles.animation || ""; + + elementStyles.delete(element); + } +} diff --git a/src/visualBuilder/utils/enableInlineEditing.ts b/src/visualBuilder/utils/enableInlineEditing.ts index ea020a92..a1b5fed8 100644 --- a/src/visualBuilder/utils/enableInlineEditing.ts +++ b/src/visualBuilder/utils/enableInlineEditing.ts @@ -10,6 +10,7 @@ import { handleFieldInput, handleFieldKeyDown } from "./handleFieldMouseDown"; import { FieldDataType, VisualBuilderEditContext } from "./types/index.types"; import { updateFocussedState } from "./updateFocussedState"; import { pasteAsPlainText } from "./pasteAsPlainText"; +import { clearVisibilityStyles } from "./clearStyles"; export function enableInlineEditing({ expectedFieldData, @@ -31,7 +32,6 @@ export function enableInlineEditing({ const elementComputedDisplay = window.getComputedStyle(actualEditableField).display; - let textContent = (editableElement as HTMLElement).innerText || editableElement.textContent || @@ -42,8 +42,9 @@ export function enableInlineEditing({ actualEditableField.addEventListener("paste", pasteAsPlainText); } const expectedTextContent = expectedFieldData; - - const isFieldLastEdited = document.querySelector("[data-cs-last-edited]") === editableElement; + + const isFieldLastEdited = + document.querySelector("[data-cs-last-edited]") === editableElement; if ( (expectedTextContent && textContent !== expectedTextContent) || isEllipsisActive(editableElement as HTMLElement) || @@ -52,10 +53,12 @@ export function enableInlineEditing({ // TODO: Testing will be done in the E2E. const pseudoEditableField = generatePseudoEditableElement( { editableElement: editableElement as HTMLElement }, - { textContent: expectedFieldData } + { textContent: expectedFieldData } ); - (editableElement as HTMLElement).style.visibility = "hidden"; + // Hide original element immediately, disabling any transitions/animations + const originalElement = editableElement as HTMLElement; + clearVisibilityStyles(originalElement); // set field type attribute to the pseudo editable field // ensures proper keydown handling similar to the actual editable field diff --git a/src/visualBuilder/utils/handleIndividualFields.ts b/src/visualBuilder/utils/handleIndividualFields.ts index 30f4bd5c..a1ce6654 100644 --- a/src/visualBuilder/utils/handleIndividualFields.ts +++ b/src/visualBuilder/utils/handleIndividualFields.ts @@ -16,6 +16,7 @@ import { VisualBuilderEditContext } from "./types/index.types"; import { pasteAsPlainText } from "./pasteAsPlainText"; import { removeFieldToolbar } from "../generators/generateToolbar"; import { fetchEntryPermissionsAndStageDetails } from "./fetchEntryPermissionsAndStageDetails"; +import { restoreVisibilityStyles } from "./clearStyles"; /** * It handles all the fields based on their data type and its "multiple" property. @@ -154,9 +155,7 @@ export function cleanIndividualFieldResidual(elements: { pseudoEditableElement.removeEventListener("paste", pasteAsPlainText); pseudoEditableElement.remove(); if (previousSelectedEditableDOM) { - (previousSelectedEditableDOM as HTMLElement).style.removeProperty( - "visibility" - ); + restoreVisibilityStyles(previousSelectedEditableDOM as HTMLElement); } } diff --git a/src/visualBuilder/utils/truncateHandler.ts b/src/visualBuilder/utils/truncateHandler.ts new file mode 100644 index 00000000..459756af --- /dev/null +++ b/src/visualBuilder/utils/truncateHandler.ts @@ -0,0 +1,128 @@ +/** + * Utility module for handling -webkit-line-clamp, overflow, and white-space properties + * when elements are focused/unfocused in the visual builder + */ + +interface LineClampStyles { + webkitLineClamp?: string; + overflow?: string; + webkitBoxOrient?: string; + whiteSpace?: string; +} + +// WeakMap to store original styles for each element +const originalStyles = new WeakMap(); + +/** + * Removes truncate related styles from an element and stores the original values + * @param element - The element to remove line-clamp and white-space styles from + */ +export function removeTruncateStyles(element: Element): void { + if (!element || !(element instanceof HTMLElement)) return; + + const computedStyles = window.getComputedStyle(element); + const styles: LineClampStyles = {}; + + // Store original styles if they exist + if ( + computedStyles.webkitLineClamp && + computedStyles.webkitLineClamp !== "none" + ) { + styles.webkitLineClamp = computedStyles.webkitLineClamp; + } + + if (computedStyles.overflow && computedStyles.overflow !== "visible") { + styles.overflow = computedStyles.overflow; + } + + if ( + computedStyles.webkitBoxOrient && + computedStyles.webkitBoxOrient !== "horizontal" + ) { + styles.webkitBoxOrient = computedStyles.webkitBoxOrient; + } + + if (computedStyles.whiteSpace && computedStyles.whiteSpace === "nowrap") { + styles.whiteSpace = computedStyles.whiteSpace; + } + + // Only store if we found line-clamp related styles + if (Object.keys(styles).length > 0) { + originalStyles.set(element, styles); + + // Remove the styles + element.style.webkitLineClamp = "none"; + element.style.overflow = "visible"; + if (styles.webkitBoxOrient) { + element.style.webkitBoxOrient = "horizontal"; + } + if (styles.whiteSpace) { + element.style.whiteSpace = "normal"; + } + } +} + +/** + * Restores the original truncate related styles to an element + * @param element - The element to restore line-clamp and white-space styles to + */ +export function restoreTruncateStyles(element: Element): void { + if (!element || !(element instanceof HTMLElement)) return; + + const storedStyles = originalStyles.get(element); + if (!storedStyles) return; + + // Restore original styles + if (storedStyles.webkitLineClamp) { + element.style.webkitLineClamp = storedStyles.webkitLineClamp; + } else { + element.style.removeProperty("-webkit-line-clamp"); + } + + if (storedStyles.overflow) { + element.style.overflow = storedStyles.overflow; + } else { + element.style.removeProperty("overflow"); + } + + if (storedStyles.webkitBoxOrient) { + element.style.webkitBoxOrient = storedStyles.webkitBoxOrient; + } else { + element.style.removeProperty("-webkit-box-orient"); + } + + if (storedStyles.whiteSpace) { + element.style.whiteSpace = storedStyles.whiteSpace; + } else { + element.style.removeProperty("white-space"); + } + + // Clean up stored styles + originalStyles.delete(element); +} + +/** + * Checks if an element has truncate or white-space styles applied + * @param element - The element to check + * @returns boolean indicating if the element has line-clamp or white-space styles + */ +export function hasTruncateStyles(element: Element): boolean { + if (!element || !(element instanceof HTMLElement)) return false; + + const computedStyles = window.getComputedStyle(element); + return ( + computedStyles.webkitLineClamp !== "none" || + (computedStyles.overflow === "hidden" && + computedStyles.webkitBoxOrient === "vertical") || + computedStyles.whiteSpace === "nowrap" + ); +} + +/** + * Checks if an element currently has stored truncate styles + * @param element - The element to check + * @returns boolean indicating if the element has stored styles + */ +export function hasStoredLineClampStyles(element: Element): boolean { + return originalStyles.has(element); +} diff --git a/src/visualBuilder/utils/updateFocussedState.ts b/src/visualBuilder/utils/updateFocussedState.ts index a5799fab..87f2bb15 100644 --- a/src/visualBuilder/utils/updateFocussedState.ts +++ b/src/visualBuilder/utils/updateFocussedState.ts @@ -17,6 +17,11 @@ import getChildrenDirection from "./getChildrenDirection"; import { getPsuedoEditableElementStyles } from "./getPsuedoEditableStylesElement"; import { isFieldDisabled } from "./isFieldDisabled"; import { fetchEntryPermissionsAndStageDetails } from "./fetchEntryPermissionsAndStageDetails"; +import { + removeTruncateStyles, + restoreTruncateStyles, + hasStoredLineClampStyles, +} from "./truncateHandler"; interface ToolbarPositionParams { focusedToolbar: HTMLElement | null; @@ -140,6 +145,11 @@ export async function updateFocussedState({ hideHoverOutline(visualBuilderContainer); + // Remove line-clamp styles from the currently focused element + if (editableElement) { + removeTruncateStyles(editableElement); + } + // in every case, this function will bring cached values // and this should be quick const fieldSchema = await FieldSchemaMap.getFieldSchema( @@ -265,6 +275,10 @@ export function updateFocussedStateOnMutation( `[data-cslp-unique-id="${selectedElementCslpUniqueId}"]` ) || document.querySelector(`[data-cslp="${selectedElementCslp}"]`); if (!newSelectedElement && resizeObserver) { + // Restore truncate styles for the selected element before hiding focus + if (selectedElement && hasStoredLineClampStyles(selectedElement)) { + restoreTruncateStyles(selectedElement); + } hideFocusOverlay({ visualBuilderOverlayWrapper: focusOverlayWrapper, focusedToolbar,