From efe08f445fd000159858d9ab30a5f8416266611e Mon Sep 17 00:00:00 2001 From: JasonOA888 Date: Tue, 10 Mar 2026 12:02:01 +0800 Subject: [PATCH] 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. --- packages/page-controller/src/actions.ts | 48 +++++++++++++++---------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/page-controller/src/actions.ts b/packages/page-controller/src/actions.ts index 7cbcfb0..a75f0a3 100644 --- a/packages/page-controller/src/actions.ts +++ b/packages/page-controller/src/actions.ts @@ -125,23 +125,8 @@ export async function inputTextElement(element: HTMLElement, text: string) { // Clear existing content editableElement.innerText = '' - // Dispatch beforeinput event (important for React apps) - const beforeInputEvent = new InputEvent('beforeinput', { - 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. + // Dispatch keydown first (typical event order: keydown -> beforeinput -> mutation -> input -> keyup) + // Only for single-character input to maintain semantic consistency if (text.length === 1) { const keydownEvent = new KeyboardEvent('keydown', { bubbles: true, @@ -149,7 +134,30 @@ export async function inputTextElement(element: HTMLElement, text: string) { key: text, }) 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', { bubbles: true, cancelable: true, @@ -161,8 +169,10 @@ export async function inputTextElement(element: HTMLElement, text: string) { // Dispatch change event (for good measure) editableElement.dispatchEvent(new Event('change', { bubbles: true })) - // Dispatch blur and refocus to trigger any validation - editableElement.dispatchEvent(new FocusEvent('blur', { bubbles: true })) + // Trigger a real blur and a bubbling focusout to run any validation, then refocus + // Note: blur doesn't bubble, focusout does + editableElement.blur() + editableElement.dispatchEvent(new FocusEvent('focusout', { bubbles: true })) editableElement.focus() } else if (element instanceof HTMLTextAreaElement) { nativeTextAreaValueSetter.call(element, text)