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 15 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 15 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

Warning

Rate limit exceeded

@jwartofsky-yext has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 4 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7924c1b9-2311-496c-b88a-bf582d163348

📥 Commits

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

📒 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

Walkthrough

ThemeHeader was extended with multiple props (theme histories/config, modal state, counts, localDev, headDeployStatus, puckInitialHistory) and now uses a createUsePuck-based usePuck to read appState.ui.previewMode and set UI state. On mount it initializes puck history, forces interactive preview mode when needed, injects a SIDEBAR_HIDE_STYLE_ID CSS element to hide right-panel breadcrumbs/titles, and attaches per-iframe handlers (via MutationObserver) to block and sanitize link navigation inside the preview iframe. On unmount it removes the injected style and detaches/restores iframe listeners/attributes.

Sequence Diagram(s)

sequenceDiagram
  participant TH as "ThemeHeader"
  participant PS as "Puck Store"
  participant IF as "Preview Iframe"
  participant DOM as "Document (style element)"
  participant MO as "MutationObserver"

  TH->>DOM: inject SIDEBAR_HIDE_STYLE_ID (hide breadcrumbs/titles)
  TH->>PS: read appState.ui.previewMode
  alt previewMode != "interactive"
    TH->>PS: dispatch setUi(previewMode="interactive")
    PS-->>TH: updated previewMode
  end
  TH->>MO: attach observer for preview iframe content
  MO->>IF: observe mutations
  MO->>IF: strip/disable link href/target attributes
  IF-->>TH: navigation events blocked / suppressed
  Note right of TH: Component active
  TH->>MO: disconnect observer (on unmount)
  TH->>DOM: remove SIDEBAR_HIDE_STYLE_ID (on unmount)
  TH->>IF: restore original link attributes (on teardown)
Loading
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: preventing users from exiting interactive mode in Theme mode and hiding sidebar breadcrumbs, which aligns with the primary objectives of the changeset.
Description check ✅ Passed The description is directly related to the changeset, explaining why breadcrumbs are hidden and why interactive mode must be enforced, with supporting visual context.

✏️ 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: 9b2ad44

};
}, []);

useEffect(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Am cleaning this up, don't dig too deep yet

It works though

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
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