fix(page-controller): address Copilot review feedback

## Changes

1. **Check beforeinput cancellation**
   - dispatchEvent returns false if canceled
   - Check defaultPrevented as well
   - Abort mutation if event was canceled by any listener

2. **Fix event order to match real user typing**
   - Before: beforeinput -> mutation -> input -> keydown -> keyup
   - After: keydown -> beforeinput -> mutation -> input -> keyup
   - This matches typical browser event sequence

3. **Fix blur event semantics**
   - blur doesn't bubble; focusout does
   - Call editableElement.blur() to actually change focus
   - Dispatch focusout with bubbles:true for listeners
   - Then refocus

4. **Keep single-character keyboard events**
   - Already fixed in previous commit
   - Maintained here with correct order

All changes follow Copilot's suggested fixes.
This commit is contained in:
JasonOA888
2026-03-10 12:02:01 +08:00
parent 4e7f755ae9
commit efe08f445f

View File

@@ -125,23 +125,8 @@ export async function inputTextElement(element: HTMLElement, text: string) {
// Clear existing content // Clear existing content
editableElement.innerText = '' editableElement.innerText = ''
// Dispatch beforeinput event (important for React apps) // Dispatch keydown first (typical event order: keydown -> beforeinput -> mutation -> input -> keyup)
const beforeInputEvent = new InputEvent('beforeinput', { // Only for single-character input to maintain semantic consistency
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: text,
})
editableElement.dispatchEvent(beforeInputEvent)
// Set the text content
editableElement.innerText = text
// Dispatch input event (standard)
editableElement.dispatchEvent(new Event('input', { bubbles: true }))
// Dispatch keydown/keyup events for frameworks that listen to keyboard.
// To avoid inconsistent semantics, only do this for single-character input.
if (text.length === 1) { if (text.length === 1) {
const keydownEvent = new KeyboardEvent('keydown', { const keydownEvent = new KeyboardEvent('keydown', {
bubbles: true, bubbles: true,
@@ -149,7 +134,30 @@ export async function inputTextElement(element: HTMLElement, text: string) {
key: text, key: text,
}) })
editableElement.dispatchEvent(keydownEvent) editableElement.dispatchEvent(keydownEvent)
}
// Dispatch beforeinput event (important for React apps)
// Check if canceled - if so, abort the mutation
const beforeInputEvent = new InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: text,
})
const notCanceled = editableElement.dispatchEvent(beforeInputEvent)
if (!notCanceled || beforeInputEvent.defaultPrevented) {
// Listener canceled the input, abort
return
}
// Set the text content (DOM mutation)
editableElement.innerText = text
// Dispatch input event (standard)
editableElement.dispatchEvent(new Event('input', { bubbles: true }))
// Dispatch keyup after input (completing the typical event sequence)
if (text.length === 1) {
const keyupEvent = new KeyboardEvent('keyup', { const keyupEvent = new KeyboardEvent('keyup', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
@@ -161,8 +169,10 @@ export async function inputTextElement(element: HTMLElement, text: string) {
// Dispatch change event (for good measure) // Dispatch change event (for good measure)
editableElement.dispatchEvent(new Event('change', { bubbles: true })) editableElement.dispatchEvent(new Event('change', { bubbles: true }))
// Dispatch blur and refocus to trigger any validation // Trigger a real blur and a bubbling focusout to run any validation, then refocus
editableElement.dispatchEvent(new FocusEvent('blur', { bubbles: true })) // Note: blur doesn't bubble, focusout does
editableElement.blur()
editableElement.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
editableElement.focus() editableElement.focus()
} else if (element instanceof HTMLTextAreaElement) { } else if (element instanceof HTMLTextAreaElement) {
nativeTextAreaValueSetter.call(element, text) nativeTextAreaValueSetter.call(element, text)