Skip to content

fix(admin): keep code block language picker open when using its dropdown#1213

Open
ascorbic wants to merge 3 commits into
mainfrom
fix/codeblock-language-picker
Open

fix(admin): keep code block language picker open when using its dropdown#1213
ascorbic wants to merge 3 commits into
mainfrom
fix/codeblock-language-picker

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic commented May 29, 2026

What does this PR do?

Fixes the code block language picker closing as you type, making it
impossible to choose a language (#1200).

Root cause (confirmed from a DOM-mutation trace)

The bug only reproduces with a browser extension active (password manager /
autofill) -- it is fine in Incognito and in clean automation. A capture in
a real profile showed: typing a character that opens the suggestion
dropdown causes the entire react-renderer node-codeBlock node view to
be removed and re-added
-- i.e. ProseMirror recreates the React node
view. The recreation resets the node view's local isEditing state, so the
picker disappears (the blur/focusout we saw was just the focused input
being torn out of the DOM).

Why the extension matters: the extension mutates the DOM around the
picker's input as you type. ProseMirror's node view cannot reconcile that
mutation and falls back to recreating the node view. No extension -> no
overlay mutation -> no recreation, which is why it never reproduced in a
clean browser.

Fix

  • ignoreMutation on the node view (the real fix): ignore any DOM
    mutation that is not inside the editable code content
    (pre.emdash-code-block). The picker overlay -- and anything an extension
    injects into it -- no longer triggers a node view recreation. Selection
    changes and real code edits are still observed normally.
  • Outside-click hardening (a separate, latent path): the suggestion
    list is portalled to document.body, outside popoverRef, so a genuine
    click on a suggestion was treated as an outside click and dismissed the
    picker. Tag the portalled popup and treat it as part of the picker, and
    ignore untrusted (synthetic) pointer events.
  • Both decisions extracted into pure helpers (ignoreCodeBlockMutation,
    shouldDismissPicker) with unit coverage.

Closes #1200

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes (@emdash-cms/admin)
  • pnpm lint passes (pnpm lint:json -> 0 diagnostics)
  • pnpm test passes (tests/editor/CodeBlockNode.test.ts, 12 passed)
  • pnpm format has been run
  • I have added/updated tests for my changes
  • User-visible strings in the admin UI are wrapped for translation -- no new strings
  • I have added a changeset
  • New features link to an approved Discussion

AI-generated code disclosure

  • This PR includes AI-generated code -- model/tool: Claude Opus 4.8

Screenshots / test output

 Test Files  1 passed (1)
      Tests  12 passed (12)

Worth a manual check in your Chrome (the one that reproduces it) against
your local dev on this branch, since the trigger is extension-specific and
can't be exercised in clean automation.


Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks fix/codeblock-language-picker. Updated automatically when the playground redeploys.

The picker's outside-click handler closed it on any mousedown whose
target was not inside popoverRef. But the Autocomplete suggestion list
renders through Base UI's Portal at document.body, so selecting a
language counted as an outside click and tore the picker down before the
selection committed -- the reported 'loses focus and closes' behaviour
(#1200). Tag the portalled popup with a marker class and treat targets
inside it as part of the picker, and ignore untrusted (synthetic) pointer
events that browser extensions dispatch into inputs.

Extracts the decision into shouldDismissPicker() with unit coverage.
Copilot AI review requested due to automatic review settings May 29, 2026 15:07
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: bfaa04b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground bfaa04b May 29 2026, 04:53 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache bfaa04b May 29 2026, 04:53 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs bfaa04b May 29 2026, 04:53 PM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 29, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1213

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1213

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1213

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1213

emdash

npm i https://pkg.pr.new/emdash@1213

create-emdash

npm i https://pkg.pr.new/create-emdash@1213

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1213

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1213

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1213

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1213

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1213

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1213

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1213

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1213

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1213

commit: bfaa04b

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an admin editor UX bug where the code block language picker popover immediately closed when interacting with the Autocomplete suggestion dropdown (which is rendered in a portal outside the popover DOM). It does this by recognizing clicks inside the portalled popup as “inside” the picker, and by ignoring untrusted synthetic events that can be dispatched by browser extensions.

Changes:

  • Add a marker class for the portalled Autocomplete popup and treat clicks within it as not-dismiss events.
  • Ignore event.isTrusted === false mouse events in the outside-click handler to prevent extension-injected synthetic events from closing the picker.
  • Extract dismissal logic into shouldDismissPicker() and add unit tests for it.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
packages/admin/src/components/editor/CodeBlockNode.tsx Adds popup marker + shouldDismissPicker() helper; updates outside-click handling and tags the portalled dropdown content.
packages/admin/tests/editor/CodeBlockNode.test.ts Adds unit tests covering dismissal behavior for popover vs. portalled dropdown vs. true outside clicks.
.changeset/fix-codeblock-language-picker.md Adds a patch changeset describing the bugfix for the admin package.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ascorbic added 2 commits May 29, 2026 16:40
…xtension DOM mutations

Root cause of #1200, confirmed from a DOM-mutation trace: typing in the
language picker with a browser extension active (password manager /
autofill) mutates the DOM around the input, ProseMirror cannot reconcile
the mutation and recreates the entire React node view, and the remount
resets the picker's local `isEditing` state -- so it 'loses focus and
closes'. It reproduces only with an extension (fine in Incognito and in
clean automation), because only the extension mutates the overlay DOM.

Add an `ignoreMutation` to the node view that ignores any mutation not
inside the editable code content (`pre.emdash-code-block`), so the
overlay -- and anything an extension injects into it -- never triggers a
node view recreation. Selection and real code edits are still observed.

Keeps the earlier outside-click hardening (which fixes a separate path:
a genuine click on the portalled suggestion list).
…ual #1200 cause)

The DOM-mutation trace from a real profile showed the trigger: the Keeper
password manager (data-keeper-edited) activates on the language input,
injects into it, and toggles a focus guard that marks the surrounding
editor content aria-hidden. ProseMirror sees those foreign attribute
mutations on its managed nodes, redraws the document, and recreates the
code block node view -- which resets the picker's isEditing state and
closes it. It only reproduces with the extension (fine in Incognito),
which is why clean automation never triggered it.

Mark the language input and its container with the password-manager-ignore
class/attributes (keeper-ignore for Keeper; data-1p-ignore / data-lpignore /
data-bwignore / data-form-type for the others). Mirrors Kumo's
<Input passwordManagerIgnore>, which Autocomplete.InputGroup does not expose.

This is the real fix; the earlier outside-click and ignoreMutation hardening
in this PR remain as defense for the portalled-dropdown click path and stray
overlay mutations.
@github-actions github-actions Bot added the review/needs-rereview Author pushed changes since the last review label May 30, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label May 30, 2026
Copy link
Copy Markdown
Contributor

@emdashbot emdashbot Bot left a comment

Choose a reason for hiding this comment

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

This is a well-targeted, well-explained bug fix for a tricky extension-induced DOM-mutation issue. The author correctly identified that the root cause was ProseMirror recreating the React node view in response to browser-extension DOM churn, and the two-pronged fix — an ignoreMutation filter plus outside-click hardening for the portalled popup — is the right approach.

I reviewed the changed files in full and traced the logic through both new helpers (ignoreCodeBlockMutation and shouldDismissPicker) and their unit tests. The logic is sound:

  • ignoreCodeBlockMutation correctly lets ProseMirror observe only mutations inside the editable code content (pre.emdash-code-block) and ignores everything in the overlay or injected by extensions.
  • shouldDismissPicker handles the portal case by detecting clicks inside the marked popup class, and the untrusted-event guard in the mousedown listener prevents synthetic extension events from closing the picker.
  • All user-visible strings remain wrapped for Lingui, layout uses RTL-safe Tailwind (end-2, etc.), and the changeset is present.
  • The 12 new unit tests cover both helpers directly.

No issues found. Clean fix.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/admin bot:review Trigger an emdashbot code review on this PR cla: signed review/approved Approved; no new commits since size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Code block language picker loses focus when typing

2 participants