From 3081f94ff8fd615e5811e9e49064b315522ebeba Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 9 May 2024 12:43:18 -0400 Subject: [PATCH 1/2] Fix: Coerce form submitter action prop When handling form action functions, we should coerce the submitter formAction prop using the same logic as ReactDOMComponent before deciding whether it should override the action on the
. Before this fix, if the submitter's `formAction` prop was a symbol or a boolean, it would block the form's action function from firing, but the correct behavior is that symbols and booleans should be treated as if they were null/empty, because that's how we treat those values when setting DOM properties. --- .../events/plugins/FormActionEventPlugin.js | 29 +++++++++++- .../src/__tests__/ReactDOMForm-test.js | 47 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js b/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js index 8f874e2cdb8..e0431fdc8e1 100644 --- a/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js +++ b/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js @@ -14,11 +14,36 @@ import type {EventSystemFlags} from '../EventSystemFlags'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions'; +import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags'; import {getFiberCurrentPropsFromNode} from '../../client/ReactDOMComponentTree'; import {startHostTransition} from 'react-reconciler/src/ReactFiberReconciler'; +import sanitizeURL from 'react-dom-bindings/src/shared/sanitizeURL'; +import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; import {SyntheticEvent} from '../SyntheticEvent'; +function coerceFormActionProp( + actionProp: mixed, +): string | (FormData => void | Promise) | null { + // This should match the logic in ReactDOMComponent + if ( + actionProp == null || + typeof actionProp === 'symbol' || + typeof actionProp === 'boolean' + ) { + return null; + } else if (typeof actionProp === 'function') { + return (actionProp: any); + } else { + if (__DEV__) { + checkAttributeStringCoercion(actionProp, 'action'); + } + return (sanitizeURL( + enableTrustedTypesIntegration ? actionProp : '' + (actionProp: any), + ): any); + } +} + /** * This plugin invokes action functions on forms, inputs and buttons if * the form doesn't prevent default. @@ -49,9 +74,9 @@ function extractEvents( if (submitter) { const submitterProps = getFiberCurrentPropsFromNode(submitter); submitterAction = submitterProps - ? (submitterProps: any).formAction + ? coerceFormActionProp((submitterProps: any).formAction) : submitter.getAttribute('formAction'); - if (submitterAction != null) { + if (submitterAction !== null) { // The submitter overrides the form action. action = submitterAction; // If the action is a function, we don't want to pass its name diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 5db3b2fd8a5..b8ca871c9b1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -35,6 +35,7 @@ describe('ReactDOMForm', () => { let ReactDOMClient; let Scheduler; let assertLog; + let assertConsoleErrorDev; let waitForThrow; let useState; let Suspense; @@ -54,6 +55,8 @@ describe('ReactDOMForm', () => { act = require('internal-test-utils').act; assertLog = require('internal-test-utils').assertLog; waitForThrow = require('internal-test-utils').waitForThrow; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; useState = React.useState; Suspense = React.Suspense; startTransition = React.startTransition; @@ -1782,4 +1785,48 @@ describe('ReactDOMForm', () => { // The form was reset even though the action didn't finish. expect(inputRef.current.value).toBe('Initial'); }); + + test("regression: submitter's formAction prop is coerced correctly before checking if it exists", async () => { + function App({submitterAction}) { + return ( + Scheduler.log('Form action')}> +