fix(admin): shadow-DOM-aware plain keys, commit drafts before shortcut saves and safe My account links (B-WR-01, B-WR-02, B-WR-03, B-WR-06, B-IN-01)

- plain-key shortcuts test composedPath()[0], and a custom element inside a
  form counts as editable (closed shadow roots)
- Ctrl/Cmd+S and Ctrl/Cmd+Enter blur the focused field before clicking, so a
  pending taglist draft or native change is committed, then refocus it
- safeUrl keeps javascript:/data: URLs out of href and window.open
- a failed contributions GET shows a toast and keeps the sections; a section
  with error true shows an error line
- My account confirm and stale texts get their own phrase keys
- rebuilt modules/boardwalk/dist
This commit is contained in:
Jakub Zych
2026-10-23 16:29:10 +02:00
parent 44835e4c6b
commit 88559d5731
11 changed files with 92 additions and 24 deletions

View File

@@ -96,12 +96,31 @@ export function comboOf(event: KeyboardEvent): HotkeyCombo | null {
}
}
/** True when the target is inside an input, textarea, select or contenteditable element. */
/**
* True when the target is inside an input, textarea, select or contenteditable
* element, or is a custom element (a tag name with "-") inside a form: a
* plugin field type whose closed shadow root hides the real input, where
* composedPath() stops at the host.
*/
export function isEditableTarget(target: EventTarget | null): boolean {
if (!target || typeof (target as Element).closest !== 'function') {
return false
}
return (target as Element).closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])') !== null
const element = target as Element
if (element.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])') !== null) {
return true
}
return element.tagName.includes('-') && element.closest('form, [role="form"]') !== null
}
/**
* Where a key event really started: the first entry of composedPath(), which
* reaches inside an open shadow root (event.target is retargeted to the
* shadow host by the time the event reaches window), else event.target.
*/
export function originOf(event: Event): EventTarget | null {
const path = typeof event.composedPath === 'function' ? event.composedPath() : []
return path[0] ?? event.target
}
/** The keys of a shortcut as shown on screen, one chip each. */
@@ -202,7 +221,16 @@ 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()
}
}
function onPlainKey(event: KeyboardEvent): void {
@@ -210,7 +238,7 @@ function onPlainKey(event: KeyboardEvent): void {
if (combo !== 'escape' && combo !== '/' && combo !== '?') {
return
}
if (event.defaultPrevented || isEditableTarget(event.target)) {
if (event.defaultPrevented || isEditableTarget(originOf(event))) {
return
}
const doc = (event.target as Node | null)?.ownerDocument ?? document