fix(19.7): RB-WR-01 save shortcuts commit a draft inside a field type's shadow root

The save shortcuts blurred document.activeElement before clicking. With
focus on an input inside a shadow root that is the host, and blur() on a
host that is not itself focused does nothing (spec, Firefox), so a plugin
field type committing on the native change event lost the typed value.
The shortcut now focuses the button first, as a pointer press does, which
unfocuses the inner input in open and closed roots, and gives focus back
to the innermost element reached through open shadow roots.
This commit is contained in:
2026-10-23 20:27:47 +02:00
parent eea04b171e
commit 3afe9fa19c
2 changed files with 98 additions and 11 deletions

View File

@@ -221,16 +221,41 @@ function onModifierKey(event: KeyboardEvent): void {
if (event.repeat || isDisabled(target)) {
return
}
// Commit pending edits the way a pointer click would: blurring the
// focused field runs its blur and native change handlers (a taglist's
// typed-but-uncommitted tag) before the button reads the values.
const active = doc.activeElement
const blurred = active instanceof HTMLElement && active !== target && active !== doc.body ? active : null
blurred?.blur()
target.click()
if (blurred?.isConnected) {
blurred.focus()
// Commit pending edits the way a pointer click would: the press focuses
// the button, which unfocuses the field and runs its blur and native change
// handlers (a taglist's typed-but-uncommitted tag, a plugin field type that
// emits summer-change on change) before the button reads the values.
// Moving focus reaches an input inside a shadow root, open or closed;
// blur() on the shadow host does not (document.activeElement is the host).
const previous = deepestActiveElement(doc)
const restore = previous instanceof HTMLElement && previous !== target && previous !== doc.body ? previous : null
if (restore) {
target.focus({ preventScroll: true })
if (deepestActiveElement(doc) === restore) {
// The target takes no focus (not a focusable element): unfocus the
// field itself, which works once we hold the innermost element.
restore.blur()
}
}
target.click()
// Give focus back unless the click moved it somewhere else on purpose.
const after = doc.activeElement
if (restore?.isConnected && (after === target || after === doc.body || after === null)) {
restore.focus({ preventScroll: true })
}
}
/**
* The focused element, followed through open shadow roots
* (document.activeElement stops at the shadow host). A closed root stops the
* walk at its host.
*/
function deepestActiveElement(doc: Document = document): Element | null {
let active: Element | null = doc.activeElement
while (active?.shadowRoot?.activeElement) {
active = active.shadowRoot.activeElement
}
return active
}
function onPlainKey(event: KeyboardEvent): void {