Skip to content

fix: prevent user from exiting interactive mode while in Theme mode and hide sidebar breadcrumbs#1073

Open
jwartofsky-yext wants to merge 19 commits intomainfrom
fixJSONInThemeEditor
Open

fix: prevent user from exiting interactive mode while in Theme mode and hide sidebar breadcrumbs#1073
jwartofsky-yext wants to merge 19 commits intomainfrom
fixJSONInThemeEditor

Conversation

@jwartofsky-yext
Copy link
Contributor

@jwartofsky-yext jwartofsky-yext commented Mar 2, 2026

Hides the page title in the theme editor sidebar since we shouldn't ever select anything besides "Page"

Prevents the user from leaving Interactive mode while in the theme editor

If they leave interactive mode, they can click on components and enter a strange state

image

The user is not supposed to select comopnents, so this
always just displays "Page"

There is a bug that makes JSON appear in this section when
the user is able to select components in the theme editor.

There will be a separate PR to fix this issue, but this
prevents it from being visible.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

ThemeHeaderProps were expanded with several new props (themeHistories/config, setThemeHistories, clearThemeHistory, puckInitialHistory, clearLocalChangesModalOpen, setClearLocalChangesModalOpen, totalEntityCount, localDev, headDeployStatus). ThemeHeader now imports and uses createUsePuck to create usePuck, initializes puck history on mount, and enforces interactive preview mode via the puck store. The component injects a style element (SIDEBAR_HIDE_STYLE_ID) to hide right-panel breadcrumbs/titles and removes it on unmount. A new preview-frame link-blocking utility was added and wired (from InternalThemeEditor) to disable navigation inside the preview iframe by removing/restoring href/target attributes and observing DOM changes; observers and listeners are cleaned up on teardown.

Sequence Diagram(s)

sequenceDiagram
  participant TH as "ThemeHeader / InternalThemeEditor"
  participant PS as "Puck Store (usePuck)"
  participant IF as "Preview Iframe (contentDocument)"
  participant MO as "MutationObserver / Link Blocker"
  participant DOM as "Document (style element)"

  TH->>PS: create/usePuck -> read appState.ui.previewMode
  TH->>PS: initialize puck history (puckInitialHistory)
  alt previewMode != "interactive"
    TH->>PS: dispatch setUi(previewMode="interactive")
    PS-->>TH: previewMode updated
  end
  TH->>DOM: inject SIDEBAR_HIDE_STYLE_ID (hide breadcrumbs/titles)
  TH->>MO: createPreviewFrameLinkBlocker() -> bind to iframe
  MO->>IF: scan document, disable href/target on link-like elements
  MO->>IF: attach MutationObserver to keep links disabled
  IF-->>TH: navigation events prevented / suppressed
  Note right of TH: Component active
  TH->>MO: disconnect blocker & observer (on unmount)
  TH->>IF: restore original href/target attributes
  TH->>DOM: remove SIDEBAR_HIDE_STYLE_ID (on unmount)
Loading

Suggested reviewers

  • mkilpatrick
  • asanehisa
  • briantstephan
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the two main changes: preventing users from exiting interactive mode in Theme mode and hiding sidebar breadcrumbs, which align with the changeset modifications.
Description check ✅ Passed The description clearly relates to the changeset by explaining why the changes were made: hiding the page title and preventing users from leaving Interactive mode to avoid an inconsistent state.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fixJSONInThemeEditor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx (2)

97-107: Consider adding getPuck to the dependency array.

The getPuck function is used inside the effect but not listed in the dependency array. While useGetPuck() likely returns a stable reference, the exhaustive-deps ESLint rule would flag this. If the reference is guaranteed stable, consider adding a comment or suppressing the lint warning explicitly.

♻️ Suggested change
   useEffect(() => {
     // Keep theme mode in interactive preview so links/buttons are clickable
     // and Puck block selection is disabled.
     if (previewMode !== "interactive") {
       const { dispatch } = getPuck();
       dispatch({
         type: "setUi",
         ui: { previewMode: "interactive" },
       });
     }
-  }, [previewMode]);
+  }, [previewMode, getPuck]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx` around
lines 97 - 107, The useEffect in ThemeHeader uses getPuck() but doesn't include
it in the dependency array, which will trigger exhaustive-deps linting; either
add getPuck to the dependency array of the useEffect or, if getPuck is provably
stable (from useGetPuck), add an explicit eslint-disable-next-line comment (or
// eslint-disable-next-line react-hooks/exhaustive-deps) with a brief
justification; update the effect around the dispatch call (the useEffect that
references previewMode and getPuck) accordingly to satisfy the linter while
preserving the existing behavior.

109-131: Same dependency consideration for getPuck.

Similar to the previous effect, getPuck is used inside the event handler but not in the dependency array. If getPuck reference can change, this would cause a stale closure. For consistency with the other effect, consider adding it to the dependencies.

♻️ Suggested change
   useEffect(() => {
     // Prevent Puck's built-in Cmd/Ctrl+I toggle from switching to edit mode.
     const onKeyDown = (event: KeyboardEvent) => {
       const isPreviewToggle =
         (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "i";
       if (!isPreviewToggle) {
         return;
       }

       event.preventDefault();
       event.stopPropagation();
       const { dispatch } = getPuck();
       dispatch({
         type: "setUi",
         ui: { previewMode: "interactive" },
       });
     };

     window.addEventListener("keydown", onKeyDown, true);
     return () => {
       window.removeEventListener("keydown", onKeyDown, true);
     };
-  }, []);
+  }, [getPuck]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx` around
lines 109 - 131, The effect registers an onKeyDown handler that calls getPuck()
but the hook's dependency array is empty, risking a stale closure if getPuck can
change; update the useEffect to include getPuck in its dependency array (or
derive a stable ref to getPuck and use that) so the handler always calls the
latest getPuck, and ensure the cleanup still removes the correct listener
(referencing useEffect, onKeyDown, and getPuck).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx`:
- Around line 97-107: The useEffect in ThemeHeader uses getPuck() but doesn't
include it in the dependency array, which will trigger exhaustive-deps linting;
either add getPuck to the dependency array of the useEffect or, if getPuck is
provably stable (from useGetPuck), add an explicit eslint-disable-next-line
comment (or // eslint-disable-next-line react-hooks/exhaustive-deps) with a
brief justification; update the effect around the dispatch call (the useEffect
that references previewMode and getPuck) accordingly to satisfy the linter while
preserving the existing behavior.
- Around line 109-131: The effect registers an onKeyDown handler that calls
getPuck() but the hook's dependency array is empty, risking a stale closure if
getPuck can change; update the useEffect to include getPuck in its dependency
array (or derive a stable ref to getPuck and use that) so the handler always
calls the latest getPuck, and ensure the cleanup still removes the correct
listener (referencing useEffect, onKeyDown, and getPuck).

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f6a59b and 8632d61.

📒 Files selected for processing (1)
  • packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx

@jwartofsky-yext jwartofsky-yext changed the title WIP: fix JSON and exiting interactive mode in theme-editor fix: prevent user from exiting interactive mode while in Theme mode and hide sidebar breadcrumbs Mar 2, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx (2)

97-107: Missing getPuck in dependency array.

getPuck is called inside this effect but is not listed in the dependency array. If getPuck reference changes between renders, the effect would use a stale closure.

The same pattern appears in the effects at lines 61-74 and 143-149. If useGetPuck is guaranteed to return a stable reference (like Redux's useDispatch), this is safe but should be documented or added to the deps for clarity.

♻️ Suggested fix
   useEffect(() => {
     // Keep theme mode in interactive preview so links/buttons are clickable
     // and Puck component selection is disabled.
     if (previewMode !== "interactive") {
       const { dispatch } = getPuck();
       dispatch({
         type: "setUi",
         ui: { previewMode: "interactive" },
       });
     }
-  }, [previewMode]);
+  }, [previewMode, getPuck]);

Apply the same pattern to the other effects using getPuck at lines 61-74 and 143-149.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx` around
lines 97 - 107, The useEffect hooks that call getPuck (the blocks at/around
ThemeHeader where previewMode is set and the other effects at lines 61-74 and
143-149) are missing getPuck in their dependency arrays; add getPuck to each
effect's dependency list or, if useGetPuck (the hook providing getPuck)
guarantees a stable reference, explicitly document that guarantee and wrap
getPuck in useCallback/useRef in the ThemeHeader component to ensure stability.
Locate the effects that reference getPuck (the ones that dispatch setUi/other
actions) and either include getPuck in the deps or stabilize it via
useCallback/useRef so the effects won’t close over a stale getPuck reference.

76-95: CSS attribute selectors are fragile and may break on library updates.

The [class*='SidebarSection-breadcrumbs'] and [class*='SidebarSection-title'] selectors rely on internal CSS class naming from @puckeditor/core. These could silently break if the library updates its class naming convention.

Consider adding a comment noting the Puck version this targets, or check if Puck exposes a more stable API to hide these elements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx` around
lines 76 - 95, The injected CSS in ThemeHeader.tsx inside the useEffect uses
fragile attribute selectors ([class*='SidebarSection-breadcrumbs'] and
[class*='SidebarSection-title']) tied to `@puckeditor/core` internals; update this
by first trying to use any stable Puck API to hide breadcrumbs/titles (if
available) and only fall back to the style injection if no API exists, and add a
clear comment stating the exact Puck version this fallback targets; reference
the useEffect block and the SIDEBAR_HIDE_STYLE_ID constant so reviewers can find
and update the fallback later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx`:
- Around line 97-107: The useEffect hooks that call getPuck (the blocks
at/around ThemeHeader where previewMode is set and the other effects at lines
61-74 and 143-149) are missing getPuck in their dependency arrays; add getPuck
to each effect's dependency list or, if useGetPuck (the hook providing getPuck)
guarantees a stable reference, explicitly document that guarantee and wrap
getPuck in useCallback/useRef in the ThemeHeader component to ensure stability.
Locate the effects that reference getPuck (the ones that dispatch setUi/other
actions) and either include getPuck in the deps or stabilize it via
useCallback/useRef so the effects won’t close over a stale getPuck reference.
- Around line 76-95: The injected CSS in ThemeHeader.tsx inside the useEffect
uses fragile attribute selectors ([class*='SidebarSection-breadcrumbs'] and
[class*='SidebarSection-title']) tied to `@puckeditor/core` internals; update this
by first trying to use any stable Puck API to hide breadcrumbs/titles (if
available) and only fall back to the style injection if no API exists, and add a
clear comment stating the exact Puck version this fallback targets; reference
the useEffect block and the SIDEBAR_HIDE_STYLE_ID constant so reviewers can find
and update the fallback later.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8632d61 and 2ee2599.

⛔ Files ignored due to path filters (3)
  • packages/visual-editor/src/components/testing/screenshots/Locator/[desktop] latest version default props.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Locator/[mobile] latest version default props.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
  • packages/visual-editor/src/components/testing/screenshots/Locator/[tablet] latest version default props.png is excluded by !**/*.png, !packages/visual-editor/src/components/testing/screenshots/**
📒 Files selected for processing (1)
  • packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx

@jwartofsky-yext jwartofsky-yext added the create-dev-release Triggers dev release workflow label Mar 3, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx (1)

99-119: ⚠️ Potential issue | 🟠 Major

Add cleanup and retry logic to pointer-blocking effect.

The effect runs once on mount. If #preview-frame isn't ready, pointer blocking is never applied. Additionally, the style added to the iframe is never removed on unmount, causing behavior to leak after the component unmounts.

The first useEffect in this component (lines 78–93) demonstrates the cleanup pattern; this effect should follow the same pattern.

Suggested fix
 useEffect(() => {
-  const puckPreview =
-    document.querySelector<HTMLIFrameElement>("#preview-frame");
-  if (
-    puckPreview?.contentDocument?.head &&
-    !puckPreview?.contentDocument.getElementById(
-      PREVIEW_DISABLE_POINTER_STYLE_ID
-    )
-  ) {
-    // add this style to preview iFrame to prevent clicking or hover effects.
-    const style = puckPreview.contentDocument.createElement("style");
-    style.id = PREVIEW_DISABLE_POINTER_STYLE_ID;
-    style.innerHTML = `
-      * {
-        cursor: default !important;
-        pointer-events: none !important;
-      }
-    `;
-    puckPreview.contentDocument.head.appendChild(style);
-  }
+  const applyPointerBlock = () => {
+    const puckPreview =
+      document.querySelector<HTMLIFrameElement>("#preview-frame");
+    if (
+      !puckPreview?.contentDocument?.head ||
+      puckPreview.contentDocument.getElementById(
+        PREVIEW_DISABLE_POINTER_STYLE_ID
+      )
+    ) {
+      return;
+    }
+
+    const style = puckPreview.contentDocument.createElement("style");
+    style.id = PREVIEW_DISABLE_POINTER_STYLE_ID;
+    style.textContent = `
+      * {
+        cursor: default !important;
+        pointer-events: none !important;
+      }
+    `;
+    puckPreview.contentDocument.head.appendChild(style);
+  };
+
+  applyPointerBlock();
+  window.addEventListener("load", applyPointerBlock);
+
+  return () => {
+    window.removeEventListener("load", applyPointerBlock);
+    document
+      .querySelector<HTMLIFrameElement>("#preview-frame")
+      ?.contentDocument?.getElementById(PREVIEW_DISABLE_POINTER_STYLE_ID)
+      ?.remove();
+  };
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx` around
lines 99 - 119, The pointer-blocking effect in ThemeHeader (the useEffect that
queries "#preview-frame" and creates a style with id
PREVIEW_DISABLE_POINTER_STYLE_ID) needs retry and cleanup: change the effect to
attempt attaching the style repeatedly until the iframe document is available
(e.g., setInterval retry with short delay), ensure you only append one style
(check getElementById), store references to the created <style> element and the
interval timer, and on cleanup clear the interval and remove the injected style
from puckPreview.contentDocument (or from the element with
PREVIEW_DISABLE_POINTER_STYLE_ID) so pointer-blocking is removed when the
component unmounts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx`:
- Around line 99-119: The pointer-blocking effect in ThemeHeader (the useEffect
that queries "#preview-frame" and creates a style with id
PREVIEW_DISABLE_POINTER_STYLE_ID) needs retry and cleanup: change the effect to
attempt attaching the style repeatedly until the iframe document is available
(e.g., setInterval retry with short delay), ensure you only append one style
(check getElementById), store references to the created <style> element and the
interval timer, and on cleanup clear the interval and remove the injected style
from puckPreview.contentDocument (or from the element with
PREVIEW_DISABLE_POINTER_STYLE_ID) so pointer-blocking is removed when the
component unmounts.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ee2599 and e60adac.

📒 Files selected for processing (1)
  • packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 3, 2026

commit: 3ef54d4

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx (1)

261-265: Consider scoping the MutationObserver to a narrower parent if possible.

Observing the entire document with childList: true, subtree: true can be expensive in complex DOMs. If the preview iframe is always inserted into a known container, observing that container instead would reduce overhead.

That said, this is a reasonable approach given the need to handle dynamic iframe insertion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx` around
lines 261 - 265, The MutationObserver is currently observing the whole document
(iframeObserver.observe(document, ...)), which is potentially expensive; change
it to observe a narrower known container element (e.g., the preview iframe
parent/container node) by locating that element first (query selector or
existing ref) and calling iframeObserver.observe(container, { childList: true,
subtree: true }) and fallback to observing document only if the container is not
found; update the logic around syncPreviewFrame/iframeObserver to locate the
container (use the container's unique selector or ref), and ensure you
disconnect iframeObserver in the component cleanup/unmount to avoid leaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx`:
- Around line 261-265: The MutationObserver is currently observing the whole
document (iframeObserver.observe(document, ...)), which is potentially
expensive; change it to observe a narrower known container element (e.g., the
preview iframe parent/container node) by locating that element first (query
selector or existing ref) and calling iframeObserver.observe(container, {
childList: true, subtree: true }) and fallback to observing document only if the
container is not found; update the logic around syncPreviewFrame/iframeObserver
to locate the container (use the container's unique selector or ref), and ensure
you disconnect iframeObserver in the component cleanup/unmount to avoid leaks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1484da44-830c-4894-b6d9-1c85b2b59262

📥 Commits

Reviewing files that changed from the base of the PR and between e60adac and 9d1d607.

📒 Files selected for processing (1)
  • packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx

@jwartofsky-yext jwartofsky-yext requested a review from benlife5 March 4, 2026 16:51
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/visual-editor/src/internal/utils/previewFrameLinkBlocker.ts`:
- Around line 72-75: The MutationObserver invocation at
anchorObserver.observe(previewDocument.documentElement, { childList: true,
subtree: true }) misses attribute changes on existing links; update the observe
options for the observer in previewFrameLinkBlocker.ts (the
anchorObserver.observe call) to also watch attribute mutations by adding
attributes: true and attributeFilter: ['href','target'] (keep subtree and
childList as-is) so in-place href/target updates are detected.
- Around line 175-181: syncPreviewFrame currently returns early when
document.getElementById(PUCK_PREVIEW_IFRAME_ID) is missing but does not release
the existing frame listener/blocker; before returning from syncPreviewFrame when
previewFrame is null, invoke the module's teardown/cleanup logic to remove the
current frame listener and deactivate any active blocker (e.g., call the
existing cleanup functions used elsewhere in this file such as
detachCurrentFrameListener() / deactivateBlocker() or the equivalents you have
defined) so the listener and blocker are properly released.
- Around line 17-39: The realm-sensitive instanceof checks in isLinkLikeTarget
and preventLinkNavigation cause iframe-origin events/elements to be misdetected;
replace those instanceof usages with duck-typing and event-type checks: in
isLinkLikeTarget, treat event.target as an Element-like object by verifying it
exists and has a callable closest method (e.g., check target && typeof (target
as any).closest === "function") before calling closest to detect "a, area,
[role='link']"; in preventLinkNavigation, detect keyboard activation by
examining event.type (e.g., "keydown"/"keypress") and the presence of a "key"
property on the event object rather than using instanceof KeyboardEvent, then
continue to gate on keys "Enter" and " " as before; apply these changes to the
isLinkLikeTarget and preventLinkNavigation functions so iframe-realm objects are
handled robustly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 245c9f2c-a8f1-470d-874d-abff076d9a62

📥 Commits

Reviewing files that changed from the base of the PR and between 9d1d607 and 0da19b6.

📒 Files selected for processing (3)
  • packages/visual-editor/src/internal/components/InternalThemeEditor.tsx
  • packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx
  • packages/visual-editor/src/internal/utils/previewFrameLinkBlocker.ts

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

Labels

create-dev-release Triggers dev release workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants