Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/visualBuilder/generators/generateOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions src/visualBuilder/listeners/mouseClick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
237 changes: 237 additions & 0 deletions src/visualBuilder/utils/__test__/clearStyles.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
Loading
Loading