Skip to content

fix(discussions): emit CSLP_TAGS_UPDATED after variant switch so comment icons mount at correct positions#583

Open
karancs06 wants to merge 1 commit intodevelop_v4from
fix/discussion-comment-variant-cslp-highlight
Open

fix(discussions): emit CSLP_TAGS_UPDATED after variant switch so comment icons mount at correct positions#583
karancs06 wants to merge 1 commit intodevelop_v4from
fix/discussion-comment-variant-cslp-highlight

Conversation

@karancs06
Copy link
Copy Markdown
Contributor

Problem

When the user switches variants in the Visual Editor, the iframe's CSR (client-side rendered) app receives the new variant and asynchronously re-renders, mutating every data-cslp attribute in the DOM from the base CSLP format (content_type.uid.locale.field) to the variant format (v2:content_type.uid_variantId.locale.field).

The Visual Editor's DiscussionPage watches the variantId change and immediately calls highlightCommentsOnCanvas, which calls highlightCommentIconOnCanvas for each active discussion field path. That function:

  1. Queries [data-cslp="${fieldPath}"] to find the target element
  2. Reads its getBoundingClientRect() to place the icon at position: fixed

Race condition: When the visual editor fires highlights the iframe DOM has not yet settled — the data-cslp attributes still carry the old values. Two failure modes result:

  • Base → Variant: The icon is mounted against the stale base CSLP element. After the DOM re-renders to variant CSLP the element that was used as an anchor has a different or no data-cslp value; subsequent scroll events call updateHighlightedCommentIconPosition, which silently skips any icon whose target is now missing, leaving the icon permanently drifted.
  • Variant → Base: The icon's 15-retry × 60 ms window (900 ms) expires before the iframe finishes switching back to base CSLP values. The icon never appears.

Solution

1. updateVariantClassesonSettled callback (useRecalculateVariantDataCSLPValues.ts)

updateVariantClasses already uses a MutationObserver to re-apply variant CSS classes as data-cslp attributes change. We extend it to accept an optional onSettled callback that fires once the DOM has settled:

  • scheduleSettled() — called inside the MutationObserver callback after each batch of mutations; starts/restarts a CSLP_SETTLED_DEBOUNCE_MS (200 ms) debounce timer. When no further mutations arrive within the window the callback fires.
  • Fallback timerCSLP_SETTLED_FALLBACK_MS (1 500 ms) ensures onSettled always fires even when no mutations are observed (e.g. SSR-hydrated pages that have already settled by the time the observer attaches).
  • hasSettled flag — guarantees the callback is invoked exactly once regardless of whether the debounce or the fallback fires first.
// Excerpt — useRecalculateVariantDataCSLPValues.ts
export function updateVariantClasses(onSettled?: () => void): void {
    let hasSettled = false;
    let settledTimer: ReturnType<typeof setTimeout> | null = null;

    const fireSettled = () => {
        if (hasSettled) return;
        hasSettled = true;
        if (settledTimer !== null) { clearTimeout(settledTimer); settledTimer = null; }
        onSettled?.();
    };
    const scheduleSettled = () => {
        if (hasSettled) return;
        if (settledTimer !== null) clearTimeout(settledTimer);
        settledTimer = setTimeout(fireSettled, CSLP_SETTLED_DEBOUNCE_MS);
    };
    setTimeout(fireSettled, CSLP_SETTLED_FALLBACK_MS); // guaranteed delivery
    // ...inside MutationObserver callback after updateElementClasses():
    scheduleSettled();
}

2. useVariantFieldsPostMessageEvent — send CSLP_TAGS_UPDATED signal (useVariantsPostMessageEvent.ts)

The GET_VARIANT_ID handler now passes the settled callback through:

if (isSSR) {
    if (selectedVariant) { addVariantFieldClass(selectedVariant); }
    // SSR: DOM is synchronously in its final state — signal immediately
    visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.CSLP_TAGS_UPDATED);
} else {
    // CSR: wait for the user's framework to finish re-rendering
    updateVariantClasses(() => {
        visualBuilderPostMessage?.send(VisualBuilderPostMessageEvents.CSLP_TAGS_UPDATED);
    });
}

The new CSLP_TAGS_UPDATED = "cslp-tags-updated" postMessage event constant is added to postMessage.types.ts.

3. Orphan cleanup in updateHighlightedCommentIconPosition (generateHighlightedComment.tsx)

Previously, when updateHighlightedCommentIconPosition processed a scroll/resize event it silently skipped any .highlighted-comment icon whose [data-cslp] target element was no longer in the DOM — leaving permanently drifted icons after a variant switch.

// Before: silently skipped
if (targetElement) {
    icon.style.top = ...; icon.style.left = ...;
}

// After: remove orphaned icon; visual editor re-mounts after CSLP_TAGS_UPDATED
if (targetElement) {
    icon.style.top = ...; icon.style.left = ...;
} else {
    icon.remove();
}

The visual editor re-mounts all icons fresh when CSLP_TAGS_UPDATED arrives, so removing orphans here is safe.

Tests added

File Coverage
useRecalculateVariantDataCSLPValues.spec.ts (new) Fallback fires at 1 500 ms; not before 1 499 ms; fires exactly once even if called twice; no-op when no callback
useVariantsPostMessageEvent.spec.ts CSLP_TAGS_UPDATED sent immediately for SSR (variant/null); updateVariantClasses called with callback for CSR; CSLP_TAGS_UPDATED sent when callback fires
generateHighlightedComment.test.ts (new) Position update when target exists; orphan removal; mixed (valid + orphan); empty DOM no-throw; removeAllHighlightedCommentIcons

Checklist

  • Race condition (base→variant and variant→base) eliminated
  • SSR apps receive the signal synchronously (no unnecessary wait)
  • onSettled fires exactly once (idempotency guard)
  • All existing tests pass
  • New unit tests added for every changed code path

🤖 Generated with Claude Code

…ent icons mount at correct positions

When the visual editor switches variants it sends GET_VARIANT_ID to the
iframe SDK.  The user's CSR app then re-renders with the new variant data,
changing every data-cslp attribute value (e.g. from
content_type.uid.locale.field to v2:content_type.uid_variantId.locale.field).
Previously the visual editor had no way of knowing when those attributes
had settled, so it would re-mount comment icons against stale CSLP values —
icons either missed their elements entirely or were positioned using
getBoundingClientRect() on the pre-variant DOM and then failed to track
scroll because updateHighlightedCommentIconPosition could no longer find
the element by the now-stale field-path attribute.

Changes
───────

src/visualBuilder/utils/types/postMessage.types.ts
  • Add CSLP_TAGS_UPDATED = "cslp-tags-updated" to the post-message enum.
    This is the signal the SDK sends back to the visual editor once the
    iframe's data-cslp attributes have settled after a variant switch.

src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts
  • updateVariantClasses now accepts an optional onSettled callback.
  • Inside the existing MutationObserver (which already watches each
    [data-cslp] element for attribute changes), scheduleSettled() is
    called on every relevant mutation.  After a 200 ms debounce with no
    further mutations, onSettled fires — meaning the burst of re-render
    updates has finished.
  • A 1500 ms fallback timer fires onSettled regardless, so entries that
    have no variant field overrides (zero mutations) still unblock the
    visual editor.
  • hasSettled flag ensures onSettled is called at most once per
    updateVariantClasses invocation, even if both the debounce and the
    fallback race.

src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts
  • GET_VARIANT_ID handler (CSR path): passes an onSettled callback to
    updateVariantClasses that sends CSLP_TAGS_UPDATED once the DOM settles.
  • GET_VARIANT_ID handler (SSR path): sends CSLP_TAGS_UPDATED immediately
    after addVariantFieldClass because SSR classes are applied synchronously
    on the existing DOM — no re-render delay exists.
  • Removed accidental console.log left from debugging.

src/visualBuilder/generators/generateHighlightedComment.tsx
  • updateHighlightedCommentIconPosition: when a scroll/resize position
    update is attempted for an icon whose [data-cslp="${path}"] element
    is no longer found, the icon is removed from the DOM instead of being
    silently skipped.  Without this, an icon mounted just before the
    variant re-render would drift to the wrong fixed position and stop
    tracking scroll permanently.  The visual editor will re-mount it
    correctly once CSLP_TAGS_UPDATED arrives.
  • Removed accidental console.log left from debugging.

src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts
  • Four new tests in the SSR-handling describe block covering:
    1. CSLP_TAGS_UPDATED sent immediately for SSR + variant provided.
    2. CSLP_TAGS_UPDATED sent immediately for SSR + null variant.
    3. updateVariantClasses called with an onSettled callback (CSR).
    4. CSLP_TAGS_UPDATED sent when the onSettled callback fires (CSR).

src/visualBuilder/eventManager/__test__/useRecalculateVariantDataCSLPValues.spec.ts (new)
  • Four tests for the onSettled callback logic:
    1. Fires via the 1500 ms fallback when no mutations occur.
    2. Does not fire before 1499 ms.
    3. Fires exactly once even if the fallback would fire again.
    4. Does not throw when no callback is provided.

src/visualBuilder/generators/__test__/generateHighlightedComment.test.ts (new)
  • Five tests for updateHighlightedCommentIconPosition and
    removeAllHighlightedCommentIcons covering: position update when target
    exists, orphan removal when target is gone, mixed-case handling, empty
    DOM safety, and full bulk removal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@karancs06 karancs06 requested review from a team as code owners April 17, 2026 07:33
@github-actions
Copy link
Copy Markdown

🔒 Security Scan Results

ℹ️ Note: Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds.

Check Type Count (with fixes) Without fixes Threshold Result
🔴 Critical Severity 0 0 10 ✅ Passed
🟠 High Severity 0 0 25 ✅ Passed
🟡 Medium Severity 1 0 500 ✅ Passed
🔵 Low Severity 0 0 1000 ✅ Passed

⏱️ SLA Breach Summary

✅ No SLA breaches detected. All vulnerabilities are within acceptable time thresholds.

Severity Breaches (with fixes) Breaches (no fixes) SLA Threshold (with/no fixes) Status
🔴 Critical 0 0 15 / 30 days ✅ Passed
🟠 High 0 0 30 / 120 days ✅ Passed
🟡 Medium 0 0 90 / 365 days ✅ Passed
🔵 Low 0 0 180 / 365 days ✅ Passed

✅ BUILD PASSED - All security checks passed

@github-actions
Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 72.28% 8302 / 11485
🔵 Statements 72.28% 8302 / 11485
🔵 Functions 74.88% 331 / 442
🔵 Branches 85.75% 1270 / 1481
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts 23.25% 66.66% 50% 23.25% 28-32, 48-50, 55-58, 66-112, 115-162, 169-199, 203-206
src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts 98.77% 92.1% 100% 98.77% 50-51
src/visualBuilder/generators/generateHighlightedComment.tsx 42.73% 77.77% 57.14% 42.73% 22-56, 59-88, 133-137, 165-169, 184
Generated in workflow #791 for commit e189f93 by the Vitest Coverage Report Action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant