Repository navigation
Conversation
StudioBridge's "." editor shortcut skipped keys typed in an input by checking e.target, but a keydown from a field inside a shadow root (a third-party widget's form) reaches the document retargeted to the shadow host. Typing "." in that field navigated the shopper to Studio's editor chooser. Read the field from the event's composed path instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Heads-up on overlap: decocms/blocks branch |
tlgimenes
added a commit
that referenced
this pull request
Nov 10, 2026
…-safe "." shortcut (#806) Not merged — waiting for review. ## Summary `@decocms/nextjs`'s `StudioBridge` and `@decocms/tanstack`'s `DecoDocument` / `DecoPreview` become v8 only: - v7's framed handler (`scrollToComponent`, `editor::rerender`, the core `STUDIO_BRIDGE_SCRIPT` branch guarded by `window.self!==window.top`) is no longer rendered. Studio's v8 editor reaches the page through the Studio channel only. - Both render `StudioChannel` from `@decocms/blocks/preview` with the exact `studioOrigins()` (option, else `DECO_STUDIO_ORIGIN`, else `studio.decocms.com` + `studio-stg.decocms.com`), as before. - v7's "." edit shortcut stays (Studio still serves `/choose-editor`), as a separate inline script, `studioShortcut.ts`. It is off in a frame. It reads the frame state as `var w=window,p=w.parent; if(p!==w)return`, which none of Cypress's frame-busting regexes match. It keeps the same `data-deco-studio-bridge` / `data-site` / `data-shortcut` / `data-path-template` / `data-page-id` attributes and the `StudioPage` marker lookup. Why: Cypress's `modifyObstructiveCode` rewrites `window.self!==window.top` everywhere in proxied HTML. That includes the RSC payload, where a Server Component's inline script travels as a length-prefixed text row. The row comes out one byte longer than its prefix, React's Flight client misreads the stream, and every page throws an uncaught `Connection closed.`. This broke the osklen Cypress suite on deco-sites/osklenbr-faststore#1388, which is why that site rendered `StudioChannel` by hand. ## Decisions taken - No props change and no type break. `StudioBridgeProps` (nextjs) keeps `site`, `editShortcut`, `page`, `nonce` and `studioOrigins`, and all of them still do something, so nothing needed a `@deprecated` no-op. `DecoDocumentProps` / `DecoPreviewProps` are unchanged. `StudioBridgeProps` is now its own interface with the same fields, no longer `Omit<core StudioBridgeProps, "trustedOrigins">`. - `@decocms/blocks/preview` is untouched. Its `StudioBridge` (v7 handler) stays exported for anyone who renders it directly. The shortcut script lives in each binding (`packages/nextjs/src/studioShortcut.ts` and `packages/tanstack/src/studioShortcut.ts`), following the existing per-binding `studio.ts` copies. `tests/studio-shortcut.test.ts` asserts the two files are byte-identical. - The new shortcut already reads the field from `e.composedPath()[0]`, the shadow-root fix of #784, so the binding path gets it whether or not #784 lands first. #784 still matters for the core `StudioBridge`. - The shortcut uses its own global guard (`__decoStudioShortcut`). - Conventional type `feat:` (minor). ## How to spike it ```sh bun install && bun x vitest run tests/studio-shortcut.test.ts packages/nextjs packages/tanstack/src/react cd examples/nextjs-binding-smoke && bun run build && bun run smoke # "the bridge is v8 only" check ``` On a Next.js site: render `<StudioBridge site="…" />` and curl a page. It should contain `data-deco-studio-channel` and `data-shortcut`, and no `scrollToComponent` and no `window.self!==window.top`. Load the page through Cypress (or the regex rewrite) and confirm there is no "Connection closed.". Pressing "." outside a frame goes to `studio.decocms.com/choose-editor?...`. In Studio's frame the editor still gets `deco:ready` and loads its modules. ## Evidence - `tests/studio-shortcut.test.ts` (16 tests) applies Cypress's own regexes from `packages/rewriter/lib/regex-rewriter.ts`, in both modes (first-party, and `modifyObstructiveThirdPartyCode`'s expanded ones), to the nextjs `StudioBridge` markup and to the tanstack shortcut + channel markup. The output is byte-identical. As a control, the same rewrite does change the core v7 `STUDIO_BRIDGE_SCRIPT`. - The same file runs the shortcut in jsdom for both bindings. It checks: Ctrl+. opens `choose-editor` with site/domain/path/pathTemplate/pageId; a plain "." and Ctrl+Shift+E are handled; the shortcut is inactive in a frame; `editShortcut={false}` turns it off; typing in an input, and in an input inside an open shadow root, is left alone; and a `StudioPage` marker wins at keypress time. - `react.test.tsx` checks that `DecoDocument`'s Studio scripts are exactly the shortcut and the channel. `components.test.tsx` checks the same for `StudioBridge`, with no `data-origins` and no v7 handler. - Run locally on Linux: `bun run typecheck`, `bun run lint`, `bun run lint:unused`, `skills:check`, `skills:readme:check`, `bun run build` and the `nextjs-binding-smoke` build + smoke all pass. 233 tests pass across `packages/nextjs`, `packages/tanstack/src/react`, `packages/blocks/src/v8/preview` and `tests/studio-shortcut.test.ts`. - osklen spike. deco-sites/osklenbr-faststore `feat/studio-bridge-bump` was built with `next build` + `next start` on packed 8.15.0 tarballs of this branch. Home, `/about-us` and a PLP served markup that all 9 Cypress rewriter regexes leave alone. The Cypress-rewrite Playwright harness showed no page errors (no "Connection closed."). A fake Studio parent at studio.decocms.com got `deco:ready` + `deco:loaded` x2, and the wireframe selected and outlined each section and ran html-swap. "." unframed went to `choose-editor?site=osklenbr-faststore-studio&…`. "." in a field typed a ".". In a frame, "." did nothing. ## Known limitations - A v7-era Studio that still sends `scrollToComponent` / `editor::rerender` to a v8 site gets no answer. v8 Studio doesn't send them: it loads its wireframe and html-swap modules through the channel. - Other sites that carry their own copy of the v7 bridge script are not touched by this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Makes `@decocms/nextjs`'s `StudioBridge` and `@decocms/tanstack`'s `DecoDocument`/`DecoPreview` v8-only: v7's framed handler is gone, and the "." edit shortcut is now a script of its own that never compares the window with `top` or `parent`. Cypress's `modifyObstructiveCode` rewrites such comparisons in proxied HTML; when the RSC payload carries the script, the rewrite changes its length and every page fails with an uncaught "Connection closed.". **Details** - Renders the Studio channel plus the "." shortcut; props, defaults, and outside-a-frame behavior are unchanged. - `@decocms/blocks/preview` is untouched and still exports its v7 `StudioBridge`. - A v7-era Studio that still sends `scrollToComponent` or `editor::rerender` gets no answer; v8 Studio reaches the page through the channel only. <sup>Written for commit c5716dd. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/decocms/blocks/pull/806?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="View guided diff" src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a> <a href="https://www.cubic.dev/action/auto-fix/pr/decocms/blocks/806?returnTo=https%3A%2F%2Fgithub.com%2Fdecocms%2Fblocks%2Fpull%2F806&source=description" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"><img alt="Turn on auto-fix" src="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
On a page that isn't framed,
StudioBridgeturns "." into the "open in Studio" shortcut (editShortcut), and skips it while the visitor types in a field by checkinge.target. A keydown from a field inside a shadow root reaches the document retargeted to the shadow host (a plain element), so the check misses it.Reported on v7 (
LiveControls, fixed in #783): a store embeds a third-party widget whose modal lives in an open shadow root and asks for an email; typing the "." of the email navigated the shopper tostudio.decocms.com/choose-editor. v8's bridge carried the same check over.Fix
Read the field from
e.composedPath()[0](falls back toe.target). For an open shadow root the composed path starts at the real input. A closed shadow root still hides its fields.Verification
preview.test.tsx: a "." typed into an input inside an open shadow root isn't prevented, while Ctrl+"." on the host itself still opens the chooser. It fails without the fix and passes with it.bunx vitest run packages/blocks/src/v8/preview/preview.test.tsx→ 34 passed;bun run typecheckinpackages/blocksandbiome checkon the changed files pass.🤖 Generated with Claude Code
Summary by cubic
Fixes the "." editor shortcut so typing inside a field within a shadow root (a third-party widget's form) no longer checks out of the field or navigates shoppers to Studio's editor chooser.
Keydown events from fields inside a shadow root reach the document retargeted to the shadow host, so StudioBridge's check for INPUT/TEXTAREA/SELECT missed them and treated the "." as the shortcut. The bridge now reads the field from the event's composed path, falling back to
e.target. Closed shadow roots still hide their fields.Written for commit 45a3956. Summary will update on new commits.