@@ -70,6 +70,15 @@ function setSelectionRangeAfterInputHandler(
7070 }
7171}
7272
73+ const initial = Symbol ( 'initial input value/textContent' )
74+ const onBlur = Symbol ( 'onBlur' )
75+ declare global {
76+ interface Element {
77+ [ initial ] ?: string
78+ [ onBlur ] ?: EventListener
79+ }
80+ }
81+
7382/**
7483 * React tracks the changes on element properties.
7584 * This workaround tries to alter the DOM element without React noticing,
@@ -92,8 +101,39 @@ function applyNative<T extends Element, P extends keyof T>(
92101 Object . defineProperty ( element , propName , nativeDescriptor )
93102 }
94103
104+ // Keep track of the initial value to determine if a change event should be dispatched.
105+ // CONSTRAINT: We can not determine what happened between focus event and our first API call.
106+ if ( element [ initial ] === undefined ) {
107+ element [ initial ] = String ( element [ propName ] )
108+ }
109+
95110 element [ propName ] = propValue
96111
112+ // Add an event listener for the blur event to the capture phase on the window.
113+ // CONSTRAINT: Currently there is no cross-platform solution to unshift the event handler stack.
114+ // Our change event might occur after other event handlers on the blur event have been processed.
115+ if ( ! element [ onBlur ] ) {
116+ element . ownerDocument . defaultView ?. addEventListener (
117+ 'blur' ,
118+ ( element [ onBlur ] = ( ) => {
119+ const initV = element [ initial ]
120+
121+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
122+ delete element [ onBlur ]
123+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
124+ delete element [ initial ]
125+
126+ if ( String ( element [ propName ] ) !== initV ) {
127+ fireEvent . change ( element )
128+ }
129+ } ) ,
130+ {
131+ capture : true ,
132+ once : true ,
133+ } ,
134+ )
135+ }
136+
97137 if ( descriptor ) {
98138 Object . defineProperty ( element , propName , descriptor )
99139 }
0 commit comments