Skip to content

Add a Visual Editing toolbar to ContentLink - #123

Open
matjack1 wants to merge 3 commits into
masterfrom
visual-editing-toggle
Open

matjack1 wants to merge 3 commits into
masterfrom
visual-editing-toggle

Conversation

@matjack1

@matjack1 matjack1 commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

What

A floating toolbar, shown only where <ContentLink /> is rendered (i.e. in draft mode), that tells editors whether click-to-edit is on and lets them change it:

<ContentLink
  enableClickToEdit
  toggle={{ exitDraftModeUrl: '/api/draft-mode/disable' }}
/>
  • Switch to turn click-to-edit on/off, with a hint about Alt/Option. While Alt/Option is held it shows "Temporary · release ⌥ Alt to revert" instead.
  • Show editable: flashAll(true).
  • Exit preview: a link to exitDraftModeUrl, only when it's passed.
  • Collapse, down to the switch.
  • Drag it anywhere. It stays where it's dropped, kept inside the viewport (the saved position isn't changed, so it moves back when the window grows). Double-click or Home resets it; arrow keys on the grip move it.
  • Remembers the editor's on/off choice, collapsed state and position in localStorage. With the toolbar, enableClickToEdit (including hoverOnly and scrollToNearestTarget) is only the starting state for editors who haven't chosen yet.
  • Rendered in a Shadow DOM through a portal: site CSS can't reach it and it can't reach the site. Same color as the overlays (hue).
  • Renders nothing inside iframes, where the Web Previews plugin has its own toggle.
  • A11y: role="switch" + aria-checked, aria-expanded on collapse, changes (including the temporary state) announced through a polite live region.

VisualEditingToggle is also exported, for useContentLink users:

const { controller } = useContentLink();
return controller ? <VisualEditingToggle controller={controller} /> : null;

Things to review

  • useContentLink now re-renders once the controller exists (separate commit). It was only in a ref, so the returned controller was always null on the first render and never updated. Needed by the toolbar; also makes the documented controller return value usable.
  • react-dom is now a peer dependency (>= 16.12.0), for createPortal. Only the root peerDependencies entry changed in package-lock.json.
  • The toolbar needs React 17+: React 16 delegates events to document, where events from inside a Shadow DOM are retargeted to the host, so clicks wouldn't reach the handlers. Documented; the package still supports React 16 otherwise.
  • ContentLink's return type is now ReactElement | null (was null).
  • It relies on the datocms:click-to-edit:toggle event that @datocms/content-link dispatches on document, which isn't documented or exported there. The overlay color is a copy of the library's buildOverlayColors, which isn't exported either. Both would be better as public API in content-link.
  • The toolbar code is always shipped with ContentLink, even without toggle (~6KB gzipped before minification). A React.lazy split is possible if that matters.

Related: datocms/content-link#5 moves the "Open in DatoCMS" label inside the overlay box (visible in the screenshots once released).

Testing

  • npm test: 73 passed, 19 new (src/ContentLink/__tests__/): default and saved state, defaultEnabled, hoverOnly, switch + persistence + announcements, Alt temporary state vs. app-driven changes, flash, collapse persistence, exit link, keyboard moves + Home, clamping an off-screen saved position, iframe, unmount; ContentLink with and without toggle.
  • npm run build, tsc, biome check on the changed files: pass.
  • Manually in Chrome, built package + React 19.2 + @datocms/content-link 0.3.23: real clicks on the switch, drag, position restored after reload, double-click reset, collapse restored after reload, Exit preview navigation, Alt (synthetic key events) with the real controller, page CSS (button { … !important }) not leaking in.
  • Not tested: inside a Next.js app (SSR / React 18), a real touch device, a screen reader.

Screenshots

toolbar:
visual-editing-toolbar

compressed:
visual-editing-toolbar-collapsed

with Alt held:
visual-editing-toolbar-alt-held

🤖 Generated with Claude Code

matjack1 and others added 3 commits October 28, 2026 11:05
The controller is created in an effect, after the first render, and was
only kept in a ref. The returned `controller` was therefore always null
on the first render and never updated, so components couldn't render
anything that needs it.

Mirror it in state: components re-render once it exists, and again when
it's disposed or recreated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`<ContentLink toggle />` shows a floating toolbar that tells editors
whether click-to-edit is on, and lets them turn it on and off, highlight
editable elements and, with `exitDraftModeUrl`, exit draft mode.

- Remembers the editor's choice, collapsed state and position
  (localStorage). With the toolbar, `enableClickToEdit` is only the
  starting state.
- Reflects the temporary Alt/Option state, through the
  `datocms:click-to-edit:toggle` event the controller already dispatches.
- Draggable, kept inside the viewport; arrow keys / Home on the grip.
- Rendered in a Shadow DOM through a portal, in the overlays' color
  (`hue`). Nothing inside iframes, where Web Previews has its own toggle.

`VisualEditingToggle` is exported for `useContentLink` users. The portal
makes `react-dom` a peer dependency; the toolbar needs React 17+.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Node 16/18, jsdom's window.parent doesn't return window anymore after
restoring its original descriptor, so every later test thought it was
inside an iframe. Check the iframe through a helper and mock that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Oct 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/react-datocms@4b243aa

commit: 4b243aa

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.

1 participant