fix(controller): click action robust

This commit is contained in:
Simon
2026-03-31 17:41:49 +08:00
parent 6e4094565a
commit 32d6f0c74b

View File

@@ -42,19 +42,20 @@ let lastClickedElement: HTMLElement | null = null
function blurLastClickedElement() { function blurLastClickedElement() {
if (lastClickedElement) { if (lastClickedElement) {
lastClickedElement.dispatchEvent(new PointerEvent('pointerout', { bubbles: true }))
lastClickedElement.dispatchEvent(new PointerEvent('pointerleave', { bubbles: false }))
lastClickedElement.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }))
lastClickedElement.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }))
lastClickedElement.blur() lastClickedElement.blur()
lastClickedElement.dispatchEvent(
new MouseEvent('mouseout', { bubbles: true, cancelable: true })
)
lastClickedElement.dispatchEvent(
new MouseEvent('mouseleave', { bubbles: false, cancelable: true })
)
lastClickedElement = null lastClickedElement = null
} }
} }
/** /**
* Simulate a click on the element * Simulate a full click following W3C Pointer Events + UI Events spec order:
* pointerover/enter → mouseover/enter → pointerdown → mousedown → [focus] →
* pointerup → mouseup → click
*
* @private Internal method, subject to change at any time. * @private Internal method, subject to change at any time.
*/ */
export async function clickElement(element: HTMLElement) { export async function clickElement(element: HTMLElement) {
@@ -63,7 +64,6 @@ export async function clickElement(element: HTMLElement) {
lastClickedElement = element lastClickedElement = element
await scrollIntoViewIfNeeded(element) await scrollIntoViewIfNeeded(element)
// Scroll the iframe element itself into view if needed
const frame = element.ownerDocument.defaultView?.frameElement const frame = element.ownerDocument.defaultView?.frameElement
if (frame) await scrollIntoViewIfNeeded(frame) if (frame) await scrollIntoViewIfNeeded(frame)
@@ -72,23 +72,41 @@ export async function clickElement(element: HTMLElement) {
await waitFor(0.1) await waitFor(0.1)
// hover it const rect = element.getBoundingClientRect()
element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true })) const x = rect.left + rect.width / 2
element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true })) const y = rect.top + rect.height / 2
const pointerOpts = {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
pointerType: 'mouse' as const,
}
const mouseOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }
// dispatch a sequence of events to ensure all listeners are triggered // Hover — pointer events first, then mouse events (spec order)
element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })) element.dispatchEvent(new PointerEvent('pointerover', pointerOpts))
element.dispatchEvent(new PointerEvent('pointerenter', { ...pointerOpts, bubbles: false }))
element.dispatchEvent(new MouseEvent('mouseover', mouseOpts))
element.dispatchEvent(new MouseEvent('mouseenter', { ...mouseOpts, bubbles: false }))
// focus it to ensure it gets the click event // Press
element.focus() element.dispatchEvent(new PointerEvent('pointerdown', pointerOpts))
element.dispatchEvent(new MouseEvent('mousedown', mouseOpts))
element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })) // Focus is not part of the standard "undefined and varies between user agents".
element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) // Browsers implicitly focus focusable elements on mousedown as an internal behavior.
element.focus({ preventScroll: true })
// dispatch a click event // Release
// element.click() element.dispatchEvent(new PointerEvent('pointerup', pointerOpts))
element.dispatchEvent(new MouseEvent('mouseup', mouseOpts))
await waitFor(0.2) // Wait to ensure click event processing completes // Click — element.click() triggers default behaviors (e.g. <a> navigation,
// form submission) that dispatchEvent(new MouseEvent('click')) may not.
element.click()
await waitFor(0.2)
} }
/** /**