fix(page-controller): address all Copilot review feedback
## Changes 1. **Fix early return bypassing cleanup** - Remove early return when beforeinput is canceled - Use shouldInsert flag to skip mutation but continue cleanup - Ensures waitFor and blurLastClickedElement always run 2. **Fix duplicate input events** - Contenteditable path now dispatches its own input events with inputType - Skip shared input dispatch for contenteditable - Prevents double-triggering framework listeners 3. **Fix event order (mutation before events)** - Clear content now dispatches beforeinput(inputType:deleteContent) first - Then performs the clear mutation - Then dispatches input event for the deletion - Proper event-mutation ordering 4. **Single-character keyboard events remain** - PR description clarified this is intentional - Multi-character input uses bulk insertion (not per-character typing) - Maintains semantic consistency 5. **Fix duplicate focusout event** - blur() already triggers native focusout - Removed manual focusout dispatch - Prevents double validation handlers All changes follow Copilot's suggested fixes.
This commit is contained in:
@@ -119,68 +119,96 @@ export async function inputTextElement(element: HTMLElement, text: string) {
|
|||||||
// Many frameworks (React, Vue, etc.) listen to specific events.
|
// Many frameworks (React, Vue, etc.) listen to specific events.
|
||||||
const editableElement = element as HTMLElement & { innerText: string }
|
const editableElement = element as HTMLElement & { innerText: string }
|
||||||
|
|
||||||
// Focus the element first
|
try {
|
||||||
editableElement.focus()
|
// Focus the element first
|
||||||
|
editableElement.focus()
|
||||||
|
|
||||||
// Clear existing content
|
// Dispatch keydown first (typical event order: keydown -> beforeinput -> mutation -> input -> keyup)
|
||||||
editableElement.innerText = ''
|
// Only for single-character input to maintain semantic consistency
|
||||||
|
if (text.length === 1) {
|
||||||
|
const keydownEvent = new KeyboardEvent('keydown', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
key: text,
|
||||||
|
})
|
||||||
|
editableElement.dispatchEvent(keydownEvent)
|
||||||
|
}
|
||||||
|
|
||||||
// Dispatch keydown first (typical event order: keydown -> beforeinput -> mutation -> input -> keyup)
|
// Dispatch beforeinput for clearing (deleteContent)
|
||||||
// Only for single-character input to maintain semantic consistency
|
const deleteEvent = new InputEvent('beforeinput', {
|
||||||
if (text.length === 1) {
|
|
||||||
const keydownEvent = new KeyboardEvent('keydown', {
|
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
key: text,
|
inputType: 'deleteContent',
|
||||||
})
|
})
|
||||||
editableElement.dispatchEvent(keydownEvent)
|
editableElement.dispatchEvent(deleteEvent)
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch beforeinput event (important for React apps)
|
// Clear existing content (first mutation)
|
||||||
// Check if canceled - if so, abort the mutation
|
editableElement.innerText = ''
|
||||||
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)
|
// Dispatch input event for the deletion
|
||||||
editableElement.innerText = text
|
editableElement.dispatchEvent(
|
||||||
|
new InputEvent('input', {
|
||||||
|
bubbles: true,
|
||||||
|
inputType: 'deleteContent',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// Dispatch input event (standard)
|
// Dispatch beforeinput event for insertion (important for React apps)
|
||||||
editableElement.dispatchEvent(new Event('input', { bubbles: true }))
|
// Check if canceled - if so, skip the mutation but continue cleanup
|
||||||
|
const beforeInputEvent = new InputEvent('beforeinput', {
|
||||||
// Dispatch keyup after input (completing the typical event sequence)
|
|
||||||
if (text.length === 1) {
|
|
||||||
const keyupEvent = new KeyboardEvent('keyup', {
|
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
key: text,
|
inputType: 'insertText',
|
||||||
|
data: text,
|
||||||
})
|
})
|
||||||
editableElement.dispatchEvent(keyupEvent)
|
const notCanceled = editableElement.dispatchEvent(beforeInputEvent)
|
||||||
|
const shouldInsert = notCanceled && !beforeInputEvent.defaultPrevented
|
||||||
|
|
||||||
|
// Set the text content (DOM mutation) - only if not canceled
|
||||||
|
if (shouldInsert) {
|
||||||
|
editableElement.innerText = text
|
||||||
|
|
||||||
|
// Dispatch input event for the insertion
|
||||||
|
editableElement.dispatchEvent(
|
||||||
|
new InputEvent('input', {
|
||||||
|
bubbles: true,
|
||||||
|
inputType: 'insertText',
|
||||||
|
data: text,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch keyup after input (completing the typical event sequence)
|
||||||
|
if (text.length === 1) {
|
||||||
|
const keyupEvent = new KeyboardEvent('keyup', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
key: text,
|
||||||
|
})
|
||||||
|
editableElement.dispatchEvent(keyupEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch change event (for good measure)
|
||||||
|
editableElement.dispatchEvent(new Event('change', { bubbles: true }))
|
||||||
|
|
||||||
|
// Trigger blur for validation, then refocus
|
||||||
|
// blur() dispatches its own focusout event, so we don't need a duplicate
|
||||||
|
editableElement.blur()
|
||||||
|
editableElement.focus()
|
||||||
|
} finally {
|
||||||
|
// Ensure cleanup always runs, even if early return above
|
||||||
|
// This is handled by the common cleanup below
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch change event (for good measure)
|
|
||||||
editableElement.dispatchEvent(new Event('change', { 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) {
|
} else if (element instanceof HTMLTextAreaElement) {
|
||||||
nativeTextAreaValueSetter.call(element, text)
|
nativeTextAreaValueSetter.call(element, text)
|
||||||
} else {
|
} else {
|
||||||
nativeInputValueSetter.call(element, text)
|
nativeInputValueSetter.call(element, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
element.dispatchEvent(new Event('input', { bubbles: true }))
|
// Only dispatch shared input event for non-contenteditable (contenteditable has its own)
|
||||||
|
if (!isContentEditable) {
|
||||||
|
element.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
|
||||||
await waitFor(0.1)
|
await waitFor(0.1)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user