Skip to content

fix(blocks): keep the "." shortcut out of fields inside a shadow root - #784

Open
guitavano wants to merge 1 commit into
mainfrom
fix/edit-shortcut-shadow-dom
Open

guitavano wants to merge 1 commit into
mainfrom
fix/edit-shortcut-shadow-dom

Conversation

@guitavano

@guitavano guitavano commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What

On a page that isn't framed, StudioBridge turns "." into the "open in Studio" shortcut (editShortcut), and skips it while the visitor types in a field by checking e.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 to studio.decocms.com/choose-editor. v8's bridge carried the same check over.

Fix

Read the field from e.composedPath()[0] (falls back to e.target). For an open shadow root the composed path starts at the real input. A closed shadow root still hides its fields.

Verification

  • New test in 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 typecheck in packages/blocks and biome check on 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.

View guided diff Turn on auto-fix

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

Copy link
Copy Markdown
Contributor

Heads-up on overlap: decocms/blocks branch feat/studiobridge-v8-only (#806) stops @decocms/nextjs's StudioBridge (packages/nextjs/src/StudioBridge.ts) and @decocms/tanstack's DecoDocument from rendering the core StudioBridge script. Its v7 window.self!==window.top branch breaks Next.js pages under Cypress. Both bindings now render the Studio channel plus a separate "." shortcut script, studioShortcut.ts. That new script already reads the field from e.composedPath()[0] as this PR does, so the binding path is covered either way. This PR's change to the core packages/blocks/src/v8/preview/StudioBridge.ts is still needed for anyone rendering the core bridge directly. No conflict in that file: the other branch doesn't touch it.

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

2 participants