fix: prevent user from exiting interactive mode while in Theme mode and hide sidebar breadcrumbs#1073
fix: prevent user from exiting interactive mode while in Theme mode and hide sidebar breadcrumbs#1073jwartofsky-yext wants to merge 19 commits intomainfrom
Conversation
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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThemeHeaderProps 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)
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx (2)
97-107: Consider addinggetPuckto the dependency array.The
getPuckfunction is used inside the effect but not listed in the dependency array. WhileuseGetPuck()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 forgetPuck.Similar to the previous effect,
getPuckis used inside the event handler but not in the dependency array. IfgetPuckreference 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).
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx (2)
97-107: MissinggetPuckin dependency array.
getPuckis called inside this effect but is not listed in the dependency array. IfgetPuckreference 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
useGetPuckis guaranteed to return a stable reference (like Redux'suseDispatch), 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
getPuckat 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
⛔ Files ignored due to path filters (3)
packages/visual-editor/src/components/testing/screenshots/Locator/[desktop] latest version default props.pngis excluded by!**/*.png,!packages/visual-editor/src/components/testing/screenshots/**packages/visual-editor/src/components/testing/screenshots/Locator/[mobile] latest version default props.pngis excluded by!**/*.png,!packages/visual-editor/src/components/testing/screenshots/**packages/visual-editor/src/components/testing/screenshots/Locator/[tablet] latest version default props.pngis excluded by!**/*.png,!packages/visual-editor/src/components/testing/screenshots/**
📒 Files selected for processing (1)
packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx
packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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 | 🟠 MajorAdd cleanup and retry logic to pointer-blocking effect.
The effect runs once on mount. If
#preview-frameisn'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
useEffectin 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.
commit: |
packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
🧹 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
documentwithchildList: true, subtree: truecan 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
📒 Files selected for processing (1)
packages/visual-editor/src/internal/puck/components/ThemeHeader.tsx
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
packages/visual-editor/src/internal/components/InternalThemeEditor.tsxpackages/visual-editor/src/internal/puck/components/ThemeHeader.tsxpackages/visual-editor/src/internal/utils/previewFrameLinkBlocker.ts
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