Refactor DOM special cases per tags including controlled fields (#26501)

I use a shared helper when setting properties into a helper whether it's
initial or update.

I moved the special cases per tag to commit phase so we can check it
only once. This also effectively inlines getHostProps which can be done
in a single check per prop key.

The diffProperties operation is simplified to mostly just generating a
plain diff of all properties, generating an update payload. This might
generate a few more entries that are now ignored in the commit phase.
that previously would've been ignored earlier. We could skip this and
just do the whole diff in the commit phase by always scheduling a commit
phase update.

I tested the attribute table (one change documented below) and a few
select DOM fixtures.
This commit is contained in:
Sebastian Markbåge 2023-03-28 22:40:03 -04:00 committed by GitHub
parent 5cbe6258bc
commit 85de6fde51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 597 additions and 655 deletions

File diff suppressed because it is too large Load Diff

View File

@ -15,19 +15,22 @@ import {getToStringValue, toString} from './ToStringValue';
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
import {updateValueIfChanged} from './inputValueTracking';
import getActiveElement from './getActiveElement';
import assign from 'shared/assign';
import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import type {ToStringValue} from './ToStringValue';
type InputWithWrapperState = HTMLInputElement & {
export type InputWithWrapperState = HTMLInputElement & {
_wrapperState: {
initialValue: ToStringValue,
initialChecked: ?boolean,
controlled?: boolean,
...
},
checked: boolean,
value: string,
defaultChecked: boolean,
defaultValue: string,
...
};
@ -58,20 +61,6 @@ function isControlled(props: any) {
* See http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
*/
export function getHostProps(element: Element, props: Object): Object {
const node = ((element: any): InputWithWrapperState);
const checked = props.checked;
const hostProps = assign({}, props, {
defaultChecked: undefined,
defaultValue: undefined,
value: undefined,
checked: checked != null ? checked : node._wrapperState.initialChecked,
});
return hostProps;
}
export function initWrapperState(element: Element, props: Object) {
if (__DEV__) {
checkControlledValueProps('input', props);
@ -114,10 +103,13 @@ export function initWrapperState(element: Element, props: Object) {
const node = ((element: any): InputWithWrapperState);
const defaultValue = props.defaultValue == null ? '' : props.defaultValue;
const initialChecked =
props.checked != null ? props.checked : props.defaultChecked;
node._wrapperState = {
initialChecked:
props.checked != null ? props.checked : props.defaultChecked,
typeof initialChecked !== 'function' &&
typeof initialChecked !== 'symbol' &&
!!initialChecked,
initialValue: getToStringValue(
props.value != null ? props.value : defaultValue,
),
@ -312,15 +304,16 @@ export function postMountWrapper(
node.name = '';
}
if (disableInputAttributeSyncing) {
// When not syncing the checked attribute, the checked property
// never gets assigned. It must be manually set. We don't want
// to do this when hydrating so that existing user input isn't
// modified
if (!isHydrating) {
updateChecked(element, props);
}
// The checked property never gets assigned. It must be manually set.
// We don't want to do this when hydrating so that existing user input isn't
// modified
// TODO: I'm pretty sure this is a bug because initialValueTracking won't be
// correct for the hydration case then.
if (!isHydrating) {
node.checked = !!node._wrapperState.initialChecked;
}
if (disableInputAttributeSyncing) {
// Only assign the checked attribute if it is defined. This saves
// a DOM write when controlling the checked attribute isn't needed
// (text inputs, submit/reset)

View File

@ -12,7 +12,6 @@ import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCur
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
import {getToStringValue, toString} from './ToStringValue';
import assign from 'shared/assign';
import isArray from 'shared/isArray';
let didWarnValueDefaultValue;
@ -130,12 +129,6 @@ function updateOptions(
* selected.
*/
export function getHostProps(element: Element, props: Object): Object {
return assign({}, props, {
value: undefined,
});
}
export function initWrapperState(element: Element, props: Object) {
const node = ((element: any): SelectWithWrapperState);
if (__DEV__) {

View File

@ -17,7 +17,7 @@ import {disableTextareaChildren} from 'shared/ReactFeatureFlags';
let didWarnValDefaultVal = false;
type TextAreaWithWrapperState = HTMLTextAreaElement & {
export type TextAreaWithWrapperState = HTMLTextAreaElement & {
_wrapperState: {initialValue: ToStringValue},
};
@ -37,31 +37,6 @@ type TextAreaWithWrapperState = HTMLTextAreaElement & {
* `defaultValue` if specified, or the children content (deprecated).
*/
export function getHostProps(element: Element, props: Object): Object {
const node = ((element: any): TextAreaWithWrapperState);
if (props.dangerouslySetInnerHTML != null) {
throw new Error(
'`dangerouslySetInnerHTML` does not make sense on <textarea>.',
);
}
// Always set children to the same thing. In IE9, the selection range will
// get reset if `textContent` is mutated. We could add a check in setTextContent
// to only set the value if/when the value differs from the node value (which would
// completely solve this IE9 bug), but Sebastian+Sophie seemed to like this
// solution. The value can be a boolean or object so that's why it's forced
// to be a string.
const hostProps = {
...props,
value: undefined,
defaultValue: undefined,
children: toString(node._wrapperState.initialValue),
};
return hostProps;
}
export function initWrapperState(element: Element, props: Object) {
const node = ((element: any): TextAreaWithWrapperState);
if (__DEV__) {
@ -120,8 +95,10 @@ export function initWrapperState(element: Element, props: Object) {
initialValue = defaultValue;
}
const stringValue = getToStringValue(initialValue);
node.defaultValue = (stringValue: any); // This will be toString:ed.
node._wrapperState = {
initialValue: getToStringValue(initialValue),
initialValue: stringValue,
};
}

View File

@ -133,6 +133,42 @@ describe('ChangeEventPlugin', () => {
}
});
it('should not invoke a change event for textarea same value', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const node = ReactDOM.render(
<textarea onChange={cb} defaultValue="initial" />,
container,
);
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 (capture)', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const node = ReactDOM.render(
<textarea onChangeCapture={cb} defaultValue="initial" />,
container,
);
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 checkbox checked=true to be current', () => {
let called = 0;