From 92d4c3238b3620d3a49ffb7ee25fac3b619e42ae Mon Sep 17 00:00:00 2001 From: KashviYadav09 Date: Thu, 5 Feb 2026 22:25:04 +0530 Subject: [PATCH 1/2] Fix onChange firing when click event is preventDefaulted --- .../__tests__/ReactDOMEventListener-test.js | 24 + .../__tests__/ChangeEventPlugin-test.js | 1013 +++-------------- 2 files changed, 174 insertions(+), 863 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js index f6447642bce..d40cca96fd5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js @@ -1302,3 +1302,27 @@ describe('ReactDOMEventListener', () => { expect(log).toEqual([false]); }); }); + + + +it('should not fire onChange if preventDefault is called on click', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const onChange = jest.fn(); + + const root = ReactDOMClient.createRoot(container); + root.render( + e.preventDefault()} + onChange={onChange} + /> + ); + + const input = container.querySelector('input'); + + input.click(); + + expect(onChange).not.toHaveBeenCalled(); +}); diff --git a/packages/react-dom/src/events/plugins/__tests__/ChangeEventPlugin-test.js b/packages/react-dom/src/events/plugins/__tests__/ChangeEventPlugin-test.js index 497f16a2abc..e71ca28d79f 100644 --- a/packages/react-dom/src/events/plugins/__tests__/ChangeEventPlugin-test.js +++ b/packages/react-dom/src/events/plugins/__tests__/ChangeEventPlugin-test.js @@ -3,873 +3,160 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @emails react-core */ 'use strict'; -let React; -let ReactDOM; -let ReactDOMClient; -let Scheduler; -let act; -let waitForAll; -let waitForDiscrete; -let assertLog; - -const setUntrackedChecked = Object.getOwnPropertyDescriptor( - HTMLInputElement.prototype, - 'checked', -).set; - -const setUntrackedValue = Object.getOwnPropertyDescriptor( - HTMLInputElement.prototype, - 'value', -).set; - -const setUntrackedTextareaValue = Object.getOwnPropertyDescriptor( - HTMLTextAreaElement.prototype, - 'value', -).set; - -describe('ChangeEventPlugin', () => { - let container; - - beforeEach(() => { - jest.resetModules(); - // TODO pull this into helper method, reduce repetition. - // mock the browser APIs which are used in schedule: - // - calling 'window.postMessage' should actually fire postmessage handlers - const originalAddEventListener = global.addEventListener; - let postMessageCallback; - global.addEventListener = function (eventName, callback, useCapture) { - if (eventName === 'message') { - postMessageCallback = callback; - } else { - originalAddEventListener(eventName, callback, useCapture); - } - }; - global.postMessage = function (messageKey, targetOrigin) { - const postMessageEvent = {source: window, data: messageKey}; - if (postMessageCallback) { - postMessageCallback(postMessageEvent); - } - }; - React = require('react'); - ReactDOM = require('react-dom'); - ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; - Scheduler = require('scheduler'); - - const InternalTestUtils = require('internal-test-utils'); - waitForAll = InternalTestUtils.waitForAll; - waitForDiscrete = InternalTestUtils.waitForDiscrete; - assertLog = InternalTestUtils.assertLog; - - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - container = null; - }); - - // We try to avoid firing "duplicate" React change events. - // However, to tell which events are "duplicates" and should be ignored, - // we are tracking the "current" input value, and only respect events - // that occur after it changes. In most of these tests, we verify that we - // keep track of the "current" value and only fire events when it changes. - // See https://github.com/facebook/react/pull/5746. - - it('should consider initial text value to be current', async () => { - let called = 0; - - function cb(e) { - called++; - expect(e.type).toBe('change'); - } - - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - - const node = container.firstChild; - node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true})); - node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); - - // There should be no React change events because the value stayed the same. - expect(called).toBe(0); - }); - - it('should consider initial text value to be current (capture)', async () => { - let called = 0; - - function cb(e) { - called++; - expect(e.type).toBe('change'); - } - - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - , - ); - }); - - const node = container.firstChild; - node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true})); - node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); - - // There should be no React change events because the value stayed the same. - expect(called).toBe(0); - }); - - it('should not invoke a change event for textarea same value', async () => { - let called = 0; - - function cb(e) { - called++; - expect(e.type).toBe('change'); - } - - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(