fix(admin): keep code block language picker open when using its dropdown#1213
fix(admin): keep code block language picker open when using its dropdown#1213ascorbic wants to merge 3 commits into
Conversation
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.
🦋 Changeset detectedLatest commit: bfaa04b The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
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 |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | bfaa04b | May 29 2026, 04:53 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | bfaa04b | May 29 2026, 04:53 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | bfaa04b | May 29 2026, 04:53 PM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
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 === falsemouse 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.
…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.
There was a problem hiding this comment.
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:
ignoreCodeBlockMutationcorrectly lets ProseMirror observe only mutations inside the editable code content (pre.emdash-code-block) and ignores everything in the overlay or injected by extensions.shouldDismissPickerhandles 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.
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-codeBlocknode view tobe removed and re-added -- i.e. ProseMirror recreates the React node
view. The recreation resets the node view's local
isEditingstate, so thepicker disappears (the
blur/focusoutwe saw was just the focused inputbeing 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
ignoreMutationon the node view (the real fix): ignore any DOMmutation that is not inside the editable code content
(
pre.emdash-code-block). The picker overlay -- and anything an extensioninjects into it -- no longer triggers a node view recreation. Selection
changes and real code edits are still observed normally.
list is portalled to
document.body, outsidepopoverRef, so a genuineclick 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.
ignoreCodeBlockMutation,shouldDismissPicker) with unit coverage.Closes #1200
Type of change
Checklist
pnpm typecheckpasses (@emdash-cms/admin)pnpm lintpasses (pnpm lint:json-> 0 diagnostics)pnpm testpasses (tests/editor/CodeBlockNode.test.ts, 12 passed)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
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.