Fix hasFocus() for editors nested in contenteditable elements #189

Closed
excursus wants to merge 1 commit from fix-nested-contenteditable into master
excursus commented 2026-01-26 16:45:31 +01:00 (Migrated from github.com)

Problem

When a ProseMirror editor is nested inside another element with contenteditable="true", view.hasFocus() incorrectly returns false even when the editor has focus. This causes various functionality to break, including selection handling.

Cause

When the editor is inside a contenteditable ancestor, the browser sets document.activeElement to the outermost contenteditable element rather than the editor's DOM element. The existing hasFocus() implementation compares activeElement directly to this.dom, which fails in this nested scenario.

Solution

Added an effectiveActiveElement() helper method that accounts for nested contenteditable elements. When activeElement is an ancestor that contains this.dom, the helper returns this.dom as the effective active element. The hasFocus() method now uses this helper instead of directly accessing this.root.activeElement.

Testing

  • Added a new test case "reports focus correctly when nested in contenteditable" that verifies hasFocus() returns true when the editor is mounted inside a contenteditable wrapper
  • Wrapped the entire test harness in a contenteditable element to validate behavior; several tests were failing before this fix, and now all pass.
## Problem When a ProseMirror editor is nested inside another element with `contenteditable="true"`, `view.hasFocus()` incorrectly returns `false` even when the editor has focus. This causes various functionality to break, including selection handling. ## Cause When the editor is inside a contenteditable ancestor, the browser sets `document.activeElement` to the outermost contenteditable element rather than the editor's DOM element. The existing `hasFocus()` implementation compares `activeElement` directly to `this.dom`, which fails in this nested scenario. ## Solution Added an `effectiveActiveElement()` helper method that accounts for nested contenteditable elements. When `activeElement` is an ancestor that contains `this.dom`, the helper returns `this.dom` as the effective active element. The `hasFocus()` method now uses this helper instead of directly accessing `this.root.activeElement`. ## Testing - Added a new test case "reports focus correctly when nested in contenteditable" that verifies `hasFocus()` returns true when the editor is mounted inside a contenteditable wrapper - Wrapped the entire test harness in a contenteditable element to validate behavior; several tests were failing before this fix, and now all pass.
marijnh commented 2026-01-26 16:49:51 +01:00 (Migrated from github.com)

the browser sets document.activeElement to the outermost contenteditable element

Which browser does this?

> the browser sets document.activeElement to the outermost contenteditable element Which browser does this?
excursus commented 2026-01-26 17:37:22 +01:00 (Migrated from github.com)

I tested this in Chrome on OSX.

I tested this in Chrome on OSX.
marijnh commented 2026-03-20 11:06:37 +01:00 (Migrated from github.com)

I just verified that, on MacOS Chrome, focusing an editable element inside another editable element sets document.activeElement to the inner element. All other browsers behave the same. This is the test I used.

<div contenteditable=true id=outer>
Outer content
<div contenteditable=false>
  uneditable
  <div contenteditable=true id=inner>inner content</div>
</div>
</div>

Possibly you don't have an uneditable element around the inner editable element. That will make it part of the outer editable region.

I just verified that, on MacOS Chrome, focusing an editable element inside another editable element sets `document.activeElement` to the inner element. All other browsers behave the same. This is the test I used. ``` <div contenteditable=true id=outer> Outer content <div contenteditable=false> uneditable <div contenteditable=true id=inner>inner content</div> </div> </div> ``` Possibly you don't have an uneditable element around the inner editable element. That will make it part of the outer editable region.

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
prosemirror/prosemirror-view!189
No description provided.