Skip to content

VB-1556: Scroll iframe to #anchor on initial load (Visual Builder)#601

Closed
karancs06 wants to merge 1 commit into
develop_v4from
vb-1556-anchor-navigation-support
Closed

VB-1556: Scroll iframe to #anchor on initial load (Visual Builder)#601
karancs06 wants to merge 1 commit into
develop_v4from
vb-1556-anchor-navigation-support

Conversation

@karancs06
Copy link
Copy Markdown
Contributor

@karancs06 karancs06 commented May 26, 2026

VB-1556: Scroll iframe to #anchor on initial load (Visual Builder)

Ticket: VB-1556

Paired with: Visual Builder PR contentstack/visual-builder#2234

Why

When a user opens Visual Builder with a target URL containing a fragment (e.g. host/page#footer), the Visual Builder PR ensures the iframe src ends up as host/page?entry_uid=…&builder=true#footer. The browser, however, only attempts a native anchor scroll at initial document parse time — and for SPA target sites (the common case), the #footer element doesn't yet exist in the DOM at that instant. The browser's native scroll silently no-ops and never retries.

The fix has to live inside the iframe, which is what this SDK PR adds.

Screen.Recording.2026-05-26.at.2.46.24.PM.mov

What this PR adds

  1. src/visualBuilder/eventManager/useScrollToHashAnchor.ts (new file)

    • Reads window.location.hash.
    • Bails out for empty hashes and for hash-router URLs (#/route, #!/route) — those are routes, not anchors.
    • Decodes percent-encoded ids (e.g. #contact%20uscontact us).
    • Looks up the element via document.getElementById(id), falling back to document.querySelector('[name="…"]') for legacy anchors.
    • If the element exists, calls scrollIntoView({ behavior: "smooth", block: "start" }) immediately.
    • If it doesn't, sets up a MutationObserver on document.body (childList, subtree) so the scroll fires the moment the element is inserted (SPA hydration). A 5-second safety timeout disconnects the observer if the element never appears.
  2. src/visualBuilder/index.ts

    • One import + one call to useScrollToHashAnchor() in the existing BUILDER window-type block of the VisualBuilder constructor, next to useScrollToField().

How it works end-to-end

1. Visual Editor sets iframe.src = "host/page?entry_uid=…&builder=true#footer"
2. Iframe loads; live-preview-sdk initializes inside it.
3. After the post-message init handshake, useScrollToHashAnchor() runs.
4. window.location.hash === "#footer" → not a router URL → look up #footer.
   a. Element present:    scrollIntoView immediately.
   b. Element not present (SPA, hydrating): MutationObserver waits up to 5s
      for it to appear, then scrollIntoView.
   c. Element never appears: observer is disconnected on timeout.

Why scroll/parse logic lives inside the SDK and not in Visual Editor

Visual Editor is in a different window/context than the iframe. It cannot directly observe or scroll DOM nodes inside the user's page. The hook has to run in the same realm as the user's document — i.e. the SDK that's already bundled into that page.

Why the #fragment is appended after the query string in the iframe URL

(Repeated here from the paired VE PR for reviewers landing on this one first.)

URL spec (RFC 3986): scheme://host/path?query#fragment. The fragment is always last.

If #footer were placed before ?entry_uid=…, the ? would become part of the fragment, breaking everything that reads window.location.search (this SDK, the user's analytics, their router) and native anchor scrolling (browser would look for id="footer?entry_uid=…"). The current order is the only standards-compliant one.

Type of Change

  • Feature
  • Tests

Testing

Unit tests

Added src/visualBuilder/eventManager/__test__/useScrollToHashAnchor.test.ts covering:

  • No hash present → no scroll.
  • Hash-router #/route ignored.
  • Hashbang #!/route ignored.
  • Element already in DOM → scrollIntoView called immediately with { behavior: "smooth", block: "start" }.
  • Element appears later via mutation → observer fires → scrollIntoView called, observer disconnects.
  • Legacy <a name="…"> anchors found via fallback selector.
  • Percent-encoded ids (#contact%20us) decoded before lookup.
  • 5-second safety timeout disconnects observer; later DOM additions don't trigger scroll.

Run from live-preview-sdk/:

npx vitest run src/visualBuilder/eventManager/__test__/useScrollToHashAnchor.test.ts

All 8 tests pass.

Manual testing

Verified against test-resources/csr (CSR SPA target site):

  1. Open Visual Builder with a target URL containing #footer (after temporarily setting id="footer" on a footer-like element in the CSR app for local testing).
  2. Page scrolls smoothly to the #footer element after entry load.
  3. Reload preserves the anchor.
  4. Hash-router targets (#/dashboard) are not affected — no spurious scroll.

Adds useScrollToHashAnchor in the Visual Builder SDK: reads
window.location.hash, ignores hash-router URLs (#/, #!/) and
percent-decodes the id. If the anchor element is in the DOM,
scrollIntoView fires immediately; otherwise a MutationObserver
waits up to 5s for it to appear (SPA hydration), then scrolls.

Wired into the VisualBuilder constructor alongside useScrollToField.

Ticket: VB-1556
@karancs06 karancs06 requested review from a team as code owners May 26, 2026 09:03
@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 0 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 66.74% 2501 / 3747
🔵 Statements 65.62% 2543 / 3875
🔵 Functions 64.32% 449 / 698
🔵 Branches 60.83% 1494 / 2456
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/visualBuilder/index.ts 53.03% 26.76% 31.25% 53.03% 113-116, 121-130, 136-206, 214-247, 257-278, 330-332, 336, 391-394, 103-109
src/visualBuilder/eventManager/useScrollToHashAnchor.ts 91.17% 87.5% 100% 93.33% 11, 25, 27
Generated in workflow #830 for commit 7c360ea by the Vitest Coverage Report Action

@karancs06 karancs06 marked this pull request as draft May 27, 2026 11:46
@karancs06 karancs06 closed this May 29, 2026
@karancs06
Copy link
Copy Markdown
Contributor Author

this is a csr feature request not required currently

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