@@ -188,29 +188,11 @@ export const bridgeIframeLoads = (nodes: NodeList) => {
188188 * so we need to trigger React's synthetic event handlers directly.
189189 *
190190 * @param element HTML element to click.
191+ * @param clickType Optional click mode. Use 'native' to bypass React internal handlers.
191192 */
192- export const clickElement = ( element : HTMLElement ) : void => {
193+ export const clickElement = ( element : HTMLElement , clickType = '' ) : void => {
193194 const REACT_PROPS_KEY_PREFIX = '__reactProps$' ;
194-
195- // Find React internal props key on the element
196- const reactPropsKey = Object . keys ( element ) . find ( ( key ) => key . startsWith ( REACT_PROPS_KEY_PREFIX ) ) ;
197-
198- // If React props are found, try to use React's handlers
199- if ( reactPropsKey ) {
200- const reactProps = ( element as unknown as Record < string , unknown > ) [ reactPropsKey ] as {
201- onFocus ?: ( ) => void ;
202- onClick ?: ( ) => void ;
203- } | undefined ;
204-
205- if ( reactProps && typeof reactProps . onClick === 'function' ) {
206- // Call onFocus first if available, as some React components require it
207- if ( typeof reactProps . onFocus === 'function' ) {
208- reactProps . onFocus ( ) ;
209- }
210- reactProps . onClick ( ) ;
211- return ;
212- }
213- }
195+ const NATIVE_CLICK_TYPE = 'native' ;
214196
215197 // Simulate a realistic click because it may not be enough to execute element.click()
216198 // https://github.com/AdguardTeam/Scriptlets/issues/491
@@ -234,23 +216,214 @@ export const clickElement = (element: HTMLElement): void => {
234216 const noBubbleOpts : MouseEventInit = Object . assign ( { } , commonOpts , { bubbles : false } ) ;
235217 const releaseOpts : MouseEventInit = Object . assign ( { } , commonOpts , { buttons : 0 } ) ;
236218
237- // Feature-detect PointerEvent for environments that don't support it
238- const hasPointerEvent = typeof PointerEvent === 'function' ;
219+ /**
220+ * Creates a trusted-looking event proxy for either React handlers or native
221+ * inline handlers.
222+ *
223+ * React handlers need extra SyntheticEvent-like fields such as
224+ * `nativeEvent`, `persist()`, `isDefaultPrevented()` and stable
225+ * `currentTarget`. Native inline handlers only need the original event with
226+ * `isTrusted` spoofed to `true`.
227+ *
228+ * @param nativeEvent Original DOM event.
229+ * @param eventType Event type exposed to the handler.
230+ * @param isReactEvent Whether to expose React-specific SyntheticEvent-like fields.
231+ *
232+ * @returns Proxied event object with the fields needed for the target handler type.
233+ */
234+ const createEventProxy = ( nativeEvent : Event , eventType : string , isReactEvent = false ) : Event => {
235+ let defaultPrevented = nativeEvent . defaultPrevented ;
236+ let propagationStopped = false ;
237+
238+ return new Proxy ( nativeEvent , {
239+ get ( target , prop ) {
240+ if ( prop === 'isTrusted' ) {
241+ return true ;
242+ }
243+ if ( ! isReactEvent ) {
244+ const value = Reflect . get ( target , prop ) ;
245+ if ( typeof value === 'function' ) {
246+ return value . bind ( target ) ;
247+ }
239248
240- if ( hasPointerEvent ) {
241- element . dispatchEvent ( new PointerEvent ( 'pointerover' , commonOpts ) ) ;
242- element . dispatchEvent ( new PointerEvent ( 'pointerenter' , noBubbleOpts ) ) ;
243- }
244- element . dispatchEvent ( new MouseEvent ( 'mouseover' , commonOpts ) ) ;
245- element . dispatchEvent ( new MouseEvent ( 'mouseenter' , noBubbleOpts ) ) ;
246- if ( hasPointerEvent ) {
247- element . dispatchEvent ( new PointerEvent ( 'pointerdown' , commonOpts ) ) ;
248- }
249- element . dispatchEvent ( new MouseEvent ( 'mousedown' , commonOpts ) ) ;
250- element . focus ( ) ;
251- if ( hasPointerEvent ) {
252- element . dispatchEvent ( new PointerEvent ( 'pointerup' , releaseOpts ) ) ;
249+ return value ;
250+ }
251+ if ( prop === 'nativeEvent' ) {
252+ return target ;
253+ }
254+ if ( prop === 'target' || prop === 'srcElement' || prop === 'currentTarget' ) {
255+ return element ;
256+ }
257+ if ( prop === 'type' ) {
258+ return eventType ;
259+ }
260+ if ( prop === 'defaultPrevented' ) {
261+ return defaultPrevented ;
262+ }
263+ if ( prop === 'persist' ) {
264+ return ( ) => { } ;
265+ }
266+ if ( prop === 'isDefaultPrevented' ) {
267+ return ( ) => defaultPrevented ;
268+ }
269+ if ( prop === 'isPropagationStopped' ) {
270+ return ( ) => propagationStopped ;
271+ }
272+ if ( prop === 'preventDefault' ) {
273+ return ( ) => {
274+ defaultPrevented = true ;
275+ target . preventDefault ( ) ;
276+ } ;
277+ }
278+ if ( prop === 'stopPropagation' ) {
279+ return ( ) => {
280+ propagationStopped = true ;
281+ target . stopPropagation ( ) ;
282+ } ;
283+ }
284+ if ( prop === 'stopImmediatePropagation' ) {
285+ return ( ) => {
286+ propagationStopped = true ;
287+ if ( typeof target . stopImmediatePropagation === 'function' ) {
288+ target . stopImmediatePropagation ( ) ;
289+ }
290+ } ;
291+ }
292+
293+ const value = Reflect . get ( target , prop ) ;
294+ if ( typeof value === 'function' ) {
295+ return value . bind ( target ) ;
296+ }
297+
298+ return value ;
299+ } ,
300+ } ) ;
301+ } ;
302+
303+ /**
304+ * Temporarily wraps inline `on...` handlers on the clicked element so they receive
305+ * a proxied event with spoofed `isTrusted`.
306+ *
307+ * @param target Event target to inspect for inline handlers.
308+ * @param eventTypes Event types whose `on...` properties should be wrapped.
309+ *
310+ * @returns Cleanup function that restores original inline handlers.
311+ */
312+ const wrapInlineHandlers = ( target : EventTarget , eventTypes : Set < string > ) : ( ( ) => void ) => {
313+ const originalHandlers = new Map < string , OnErrorEventHandler | EventListener | null > ( ) ;
314+ const targetRecord = target as unknown as Record < string , unknown > ;
315+
316+ eventTypes . forEach ( ( eventType ) => {
317+ const propertyName = `on${ eventType } ` ;
318+ const handler = targetRecord [ propertyName ] ;
319+
320+ if ( typeof handler !== 'function' || originalHandlers . has ( propertyName ) ) {
321+ return ;
322+ }
323+
324+ originalHandlers . set ( propertyName , handler as EventListener ) ;
325+ targetRecord [ propertyName ] = function wrappedInlineHandler ( this : unknown , event : Event ) {
326+ const onEventProxy = createEventProxy ( event , eventType ) ;
327+ return handler . call ( this , onEventProxy ) ;
328+ } ;
329+ } ) ;
330+
331+ return ( ) => {
332+ originalHandlers . forEach ( ( handler , propertyName ) => {
333+ targetRecord [ propertyName ] = handler ;
334+ } ) ;
335+ } ;
336+ } ;
337+
338+ /**
339+ * Creates a focus event for the direct React handler path.
340+ *
341+ * @returns Focus event object compatible with the current environment.
342+ */
343+ const createFocusEvent = ( ) : Event => {
344+ if ( typeof FocusEvent === 'function' ) {
345+ return new FocusEvent ( 'focus' , {
346+ bubbles : false ,
347+ cancelable : false ,
348+ composed : true ,
349+ relatedTarget : null ,
350+ } ) ;
351+ }
352+
353+ return new Event ( 'focus' , {
354+ bubbles : false ,
355+ cancelable : false ,
356+ composed : true ,
357+ } ) ;
358+ } ;
359+
360+ /**
361+ * Dispatches the synthetic pointer and mouse sequence used by the native
362+ * click path while temporarily wrapping inline handlers on the bubbling path.
363+ */
364+ const dispatchNativeClick = ( ) : void => {
365+ // Feature-detect PointerEvent for environments that don't support it
366+ const hasPointerEvent = typeof PointerEvent === 'function' ;
367+ const spoofedEventTypes = new Set ( [
368+ 'click' ,
369+ 'mousedown' ,
370+ 'mouseup' ,
371+ 'mouseover' ,
372+ 'mouseenter' ,
373+ 'pointerdown' ,
374+ 'pointerup' ,
375+ 'pointerover' ,
376+ 'pointerenter' ,
377+ ] ) ;
378+ const restoreInlineHandlers = wrapInlineHandlers ( element , spoofedEventTypes ) ;
379+
380+ try {
381+ if ( hasPointerEvent ) {
382+ element . dispatchEvent ( new PointerEvent ( 'pointerover' , commonOpts ) ) ;
383+ element . dispatchEvent ( new PointerEvent ( 'pointerenter' , noBubbleOpts ) ) ;
384+ }
385+ element . dispatchEvent ( new MouseEvent ( 'mouseover' , commonOpts ) ) ;
386+ element . dispatchEvent ( new MouseEvent ( 'mouseenter' , noBubbleOpts ) ) ;
387+ if ( hasPointerEvent ) {
388+ element . dispatchEvent ( new PointerEvent ( 'pointerdown' , commonOpts ) ) ;
389+ }
390+ element . dispatchEvent ( new MouseEvent ( 'mousedown' , commonOpts ) ) ;
391+ element . focus ( ) ;
392+ if ( hasPointerEvent ) {
393+ element . dispatchEvent ( new PointerEvent ( 'pointerup' , releaseOpts ) ) ;
394+ }
395+ element . dispatchEvent ( new MouseEvent ( 'mouseup' , releaseOpts ) ) ;
396+ element . dispatchEvent ( new MouseEvent ( 'click' , releaseOpts ) ) ;
397+ } finally {
398+ restoreInlineHandlers ( ) ;
399+ }
400+ } ;
401+
402+ // Find React internal props key on the element
403+ const reactPropsKey = Object . keys ( element ) . find ( ( key ) => key . startsWith ( REACT_PROPS_KEY_PREFIX ) ) ;
404+
405+ // If React props are found, try to use React's handlers
406+ // If clickType is 'native', skip React handlers and dispatch native click directly
407+ // https://github.com/AdguardTeam/Scriptlets/issues/554
408+ if ( reactPropsKey && clickType !== NATIVE_CLICK_TYPE ) {
409+ const reactProps = ( element as unknown as Record < string , unknown > ) [ reactPropsKey ] as {
410+ onFocus ?: ( event ?: Event ) => void ;
411+ onClick ?: ( event ?: Event ) => void ;
412+ } | undefined ;
413+
414+ if ( reactProps && typeof reactProps . onClick === 'function' ) {
415+ // Call onFocus first if available, as some React components require it
416+ if ( typeof reactProps . onFocus === 'function' ) {
417+ const focusEvent = createFocusEvent ( ) ;
418+ const eventFocusProxy = createEventProxy ( focusEvent , 'focus' , true ) ;
419+ reactProps . onFocus . call ( element , eventFocusProxy ) ;
420+ }
421+ const clickEvent = new MouseEvent ( 'click' , releaseOpts ) ;
422+ const eventClickProxy = createEventProxy ( clickEvent , 'click' , true ) ;
423+ reactProps . onClick . call ( element , eventClickProxy ) ;
424+ return ;
425+ }
253426 }
254- element . dispatchEvent ( new MouseEvent ( 'mouseup' , releaseOpts ) ) ;
255- element . dispatchEvent ( new MouseEvent ( 'click' , releaseOpts ) ) ;
427+
428+ dispatchNativeClick ( ) ;
256429} ;
0 commit comments