From dcab44d757dee6fa8d40db56715732e891a3b729 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Mon, 2 Feb 2026 21:17:14 +0100 Subject: [PATCH 1/2] [react-dom] Fire onReset when automatically resetting forms (#35176) --- fixtures/dom/src/components/Header.js | 1 + .../components/fixtures/form-actions/index.js | 113 ++++++++++++++++++ .../src/client/ReactFiberConfigDOM.js | 2 + .../src/__tests__/ReactDOMForm-test.js | 51 ++++++++ 4 files changed, 167 insertions(+) create mode 100644 fixtures/dom/src/components/fixtures/form-actions/index.js diff --git a/fixtures/dom/src/components/Header.js b/fixtures/dom/src/components/Header.js index ae24b3223f64..1474d8053365 100644 --- a/fixtures/dom/src/components/Header.js +++ b/fixtures/dom/src/components/Header.js @@ -88,6 +88,7 @@ class Header extends React.Component { + diff --git a/fixtures/dom/src/components/fixtures/form-actions/index.js b/fixtures/dom/src/components/fixtures/form-actions/index.js new file mode 100644 index 000000000000..eace38ba46be --- /dev/null +++ b/fixtures/dom/src/components/fixtures/form-actions/index.js @@ -0,0 +1,113 @@ +const React = window.React; + +const {useState} = React; + +async function defer(timeoutMS) { + return new Promise(resolve => { + setTimeout(resolve, timeoutMS); + }); +} + +export default function FormActions() { + const [textValue, setTextValue] = useState('0'); + const [radioValue, setRadioValue] = useState('two'); + const [checkboxValue, setCheckboxValue] = useState([false, true, true]); + const [selectValue, setSelectValue] = useState('three'); + + return ( +
{ + await defer(500); + }} + onReset={() => { + setTextValue('0'); + setRadioValue('two'); + setCheckboxValue([false, true, true]); + setSelectValue('three'); + }}> +
+
+ type="text" + setTextValue(event.currentTarget.value)} + /> +
+
+ type="radio" + setRadioValue('one')} + /> + setRadioValue('two')} + /> + setRadioValue('three')} + /> +
+
+ type="checkbox" + { + const checked = event.currentTarget.checked; + setCheckboxValue(pending => [checked, pending[1], pending[2]]); + }} + /> + { + const checked = event.currentTarget.checked; + setCheckboxValue(pending => [pending[0], checked, pending[2]]); + }} + /> + { + const checked = event.currentTarget.checked; + setCheckboxValue(pending => [pending[0], pending[1], checked]); + }} + /> +
+
+ select + +
+
+
+ + +
+
+ ); +} diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 09653552aafe..a03ccc161ad1 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -6562,5 +6562,7 @@ export const HostTransitionContext: ReactContext = { export type FormInstance = HTMLFormElement; export function resetFormInstance(form: FormInstance): void { + ReactBrowserEventEmitterSetEnabled(true); form.reset(); + ReactBrowserEventEmitterSetEnabled(false); } diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 33988e9c4db1..f1a6cf791248 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -1596,6 +1596,57 @@ describe('ReactDOMForm', () => { expect(divRef.current.textContent).toEqual('Current username: acdlite'); }); + it('should fire onReset on automatic form reset', async () => { + const formRef = React.createRef(); + const inputRef = React.createRef(); + + let setValue; + const defaultValue = 0; + function App({promiseForUsername}) { + const [value, _setValue] = useState(defaultValue); + setValue = _setValue; + + return ( +
{ + Scheduler.log(`Async action started`); + await getText('Wait'); + }} + onReset={() => { + setValue(defaultValue); + }}> + setValue(event.currentTarget.value)} + /> +
+ ); + } + + const root = ReactDOMClient.createRoot(container); + await act(() => root.render()); + + // Dirty the controlled input + await act(() => setValue('3')); + expect(inputRef.current.value).toEqual('3'); + + // Submit the form. This will trigger an async action. + await submit(formRef.current); + assertLog(['Async action started']); + + // We haven't reset yet. + expect(inputRef.current.value).toEqual('3'); + + // Action completes. onReset has been fired and values reset manually. + await act(() => resolveText('Wait')); + assertLog([]); + expect(inputRef.current.value).toEqual('0'); + }); + it('requestFormReset schedules a form reset after transition completes', async () => { // This is the same as the previous test, except the form is updated with // a userspace action instead of a built-in form action. From 7b023d7073aa700b48674ae1985235b8c4755880 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Mon, 2 Feb 2026 21:17:31 +0100 Subject: [PATCH 2/2] [react-dom] Include `submitter` in `submit` events (#35590) --- .../src/events/SyntheticEvent.js | 11 +++++++ .../src/events/plugins/SimpleEventPlugin.js | 4 +++ .../ReactDOMEventPropagation-test.js | 3 +- .../__tests__/SimpleEventPlugin-test.js | 32 +++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/react-dom-bindings/src/events/SyntheticEvent.js b/packages/react-dom-bindings/src/events/SyntheticEvent.js index acd50cf8e0c9..11ec03091941 100644 --- a/packages/react-dom-bindings/src/events/SyntheticEvent.js +++ b/packages/react-dom-bindings/src/events/SyntheticEvent.js @@ -522,6 +522,17 @@ export const SyntheticPointerEvent: $FlowFixMe = createSyntheticEvent( PointerEventInterface, ); +/** + * @interface SubmitEvent + * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#the-submitevent-interface + */ +const SubmitEventInterface: EventInterfaceType = { + ...EventInterface, + submitter: 0, +}; +export const SyntheticSubmitEvent: $FlowFixMe = + createSyntheticEvent(SubmitEventInterface); + /** * @interface TouchEvent * @see http://www.w3.org/TR/touch-events/ diff --git a/packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js b/packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js index 8c983b3f5dfc..4873dacdbf43 100644 --- a/packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js +++ b/packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js @@ -27,6 +27,7 @@ import { SyntheticWheelEvent, SyntheticClipboardEvent, SyntheticPointerEvent, + SyntheticSubmitEvent, SyntheticToggleEvent, } from '../../events/SyntheticEvent'; @@ -162,6 +163,9 @@ function extractEvents( case 'pointerup': SyntheticEventCtor = SyntheticPointerEvent; break; + case 'submit': + SyntheticEventCtor = SyntheticSubmitEvent; + break; case 'toggle': case 'beforetoggle': // MDN claims
should not receive ToggleEvent contradicting the spec: https://html.spec.whatwg.org/multipage/indices.html#event-toggle diff --git a/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js b/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js index ebd3f9a54011..7e578fbba14e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js @@ -672,9 +672,10 @@ describe('ReactDOMEventListener', () => { reactEventType: 'submit', nativeEvent: 'submit', dispatch(node) { - const e = new Event('submit', { + const e = new SubmitEvent('submit', { bubbles: true, cancelable: true, + submitter: null, }); node.dispatchEvent(e); }, diff --git a/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js b/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js index 062762e776a5..f14fcab168b9 100644 --- a/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js +++ b/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js @@ -591,4 +591,36 @@ describe('SimpleEventPlugin', function () { ); }); }); + + it('includes the submitter in submit events', async function () { + container = document.createElement('div'); + + const onSubmit = jest.fn(event => { + event.preventDefault(); + }); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
+ +
, + ); + }); + + const submitter = container.querySelector('#submitter'); + const submitEvent = new SubmitEvent('submit', { + bubbles: true, + cancelable: true, + submitter: submitter, + }); + await act(() => { + submitter.dispatchEvent(submitEvent); + }); + + expect(onSubmit).toHaveBeenCalledTimes(1); + const event = onSubmit.mock.calls[0][0]; + expect(event.submitter).toBe(submitter); + }); });