Skip to content

Switching checked to null should leave the current value #26667

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 14 additions & 112 deletions packages/react-dom-bindings/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ export function setInitialProperties(
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);

let name = null;
let type = null;
let value = null;
let defaultValue = null;
Expand All @@ -848,31 +849,16 @@ export function setInitialProperties(
continue;
}
switch (propKey) {
case 'name': {
name = propValue;
break;
}
case 'type': {
// Fast path since 'type' is very common on inputs
if (
propValue != null &&
typeof propValue !== 'function' &&
typeof propValue !== 'symbol' &&
typeof propValue !== 'boolean'
) {
type = propValue;
if (__DEV__) {
checkAttributeStringCoercion(propValue, propKey);
}
domElement.setAttribute(propKey, propValue);
}
type = propValue;
break;
}
case 'checked': {
checked = propValue;
const checkedValue =
propValue != null ? propValue : props.defaultChecked;
const inputElement: HTMLInputElement = (domElement: any);
inputElement.checked =
!!checkedValue &&
typeof checkedValue !== 'function' &&
checkedValue !== 'symbol';
break;
}
case 'defaultChecked': {
Expand Down Expand Up @@ -904,7 +890,6 @@ export function setInitialProperties(
}
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track((domElement: any));
validateInputProps(domElement, props);
initInput(
domElement,
Expand All @@ -913,8 +898,10 @@ export function setInitialProperties(
checked,
defaultChecked,
type,
name,
false,
);
track((domElement: any));
return;
}
case 'select': {
Expand Down Expand Up @@ -1010,9 +997,9 @@ export function setInitialProperties(
}
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track((domElement: any));
validateTextareaProps(domElement, props);
initTextarea(domElement, value, defaultValue, children);
track((domElement: any));
return;
}
case 'option': {
Expand Down Expand Up @@ -1305,14 +1292,6 @@ export function updateProperties(
if (lastProps.hasOwnProperty(propKey) && lastProp != null) {
switch (propKey) {
case 'checked': {
if (!nextProps.hasOwnProperty(propKey)) {
const checkedValue = nextProps.defaultChecked;
const inputElement: HTMLInputElement = (domElement: any);
inputElement.checked =
!!checkedValue &&
typeof checkedValue !== 'function' &&
checkedValue !== 'symbol';
}
break;
}
case 'value': {
Expand Down Expand Up @@ -1341,22 +1320,6 @@ export function updateProperties(
switch (propKey) {
case 'type': {
type = nextProp;
// Fast path since 'type' is very common on inputs
if (nextProp !== lastProp) {
if (
nextProp != null &&
typeof nextProp !== 'function' &&
typeof nextProp !== 'symbol' &&
typeof nextProp !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(nextProp, propKey);
}
domElement.setAttribute(propKey, nextProp);
} else {
domElement.removeAttribute(propKey);
}
}
break;
}
case 'name': {
Expand All @@ -1365,15 +1328,6 @@ export function updateProperties(
}
case 'checked': {
checked = nextProp;
if (nextProp !== lastProp) {
const checkedValue =
nextProp != null ? nextProp : nextProps.defaultChecked;
const inputElement: HTMLInputElement = (domElement: any);
inputElement.checked =
!!checkedValue &&
typeof checkedValue !== 'function' &&
checkedValue !== 'symbol';
}
break;
}
case 'defaultChecked': {
Expand Down Expand Up @@ -1453,23 +1407,6 @@ export function updateProperties(
}
}

// Update checked *before* name.
// In the middle of an update, it is possible to have multiple checked.
// When a checked radio tries to change name, browser makes another radio's checked false.
if (
name != null &&
typeof name !== 'function' &&
typeof name !== 'symbol' &&
typeof name !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(name, 'name');
}
domElement.setAttribute('name', name);
} else {
domElement.removeAttribute('name');
}

// Update the wrapper around inputs *after* updating props. This has to
// happen after updating the rest of props. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
Expand All @@ -1481,6 +1418,7 @@ export function updateProperties(
checked,
defaultChecked,
type,
name,
);
return;
}
Expand Down Expand Up @@ -1822,33 +1760,12 @@ export function updatePropertiesWithDiff(
const propValue = updatePayload[i + 1];
switch (propKey) {
case 'type': {
// Fast path since 'type' is very common on inputs
if (
propValue != null &&
typeof propValue !== 'function' &&
typeof propValue !== 'symbol' &&
typeof propValue !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(propValue, propKey);
}
domElement.setAttribute(propKey, propValue);
} else {
domElement.removeAttribute(propKey);
}
break;
}
case 'name': {
break;
}
case 'checked': {
const checkedValue =
propValue != null ? propValue : nextProps.defaultChecked;
const inputElement: HTMLInputElement = (domElement: any);
inputElement.checked =
!!checkedValue &&
typeof checkedValue !== 'function' &&
checkedValue !== 'symbol';
break;
}
case 'defaultChecked': {
Expand Down Expand Up @@ -1916,23 +1833,6 @@ export function updatePropertiesWithDiff(
}
}

// Update checked *before* name.
// In the middle of an update, it is possible to have multiple checked.
// When a checked radio tries to change name, browser makes another radio's checked false.
if (
name != null &&
typeof name !== 'function' &&
typeof name !== 'symbol' &&
typeof name !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(name, 'name');
}
domElement.setAttribute('name', name);
} else {
domElement.removeAttribute('name');
}

// Update the wrapper around inputs *after* updating props. This has to
// happen after updating the rest of props. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
Expand All @@ -1944,6 +1844,7 @@ export function updatePropertiesWithDiff(
checked,
defaultChecked,
type,
name,
);
return;
}
Expand Down Expand Up @@ -2970,7 +2871,6 @@ export function diffHydratedProperties(
listenToNonDelegatedEvent('invalid', domElement);
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track((domElement: any));
validateInputProps(domElement, props);
// For input and textarea we current always set the value property at
// post mount to force it to diverge from attributes. However, for
Expand All @@ -2984,8 +2884,10 @@ export function diffHydratedProperties(
props.checked,
props.defaultChecked,
props.type,
props.name,
true,
);
track((domElement: any));
break;
case 'option':
validateOptionProps(domElement, props);
Expand All @@ -3008,9 +2910,9 @@ export function diffHydratedProperties(
listenToNonDelegatedEvent('invalid', domElement);
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track((domElement: any));
validateTextareaProps(domElement, props);
initTextarea(domElement, props.value, props.defaultValue, props.children);
track((domElement: any));
break;
}

Expand Down
65 changes: 60 additions & 5 deletions packages/react-dom-bindings/src/client/ReactDOMInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,30 @@ export function updateInput(
checked: ?boolean,
defaultChecked: ?boolean,
type: ?string,
name: ?string,
) {
const node: HTMLInputElement = (element: any);

// Temporarily disconnect the input from any radio buttons.
// Changing the type or name as the same time as changing the checked value
// needs to be atomically applied. We can only ensure that by disconnecting
// the name while do the mutations and then reapply the name after that's done.
node.name = '';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, we didn't used to do this in the update phase - only init phase. Not sure if it'll mess with some internal states.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be good


if (
type != null &&
typeof type !== 'function' &&
typeof type !== 'symbol' &&
typeof type !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(type, 'type');
}
node.type = type;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is always setting the type instead of only if it has changed. Not sure if that would matter like resetting dirty flags.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine? from the spec it sounds like it would only be the value sanitization algorithm but I tested https://codesandbox.io/s/crazy-ace-rc85uh?file=/src/App.js in chrome, safari, and firefox and none of them seem to do anything when reassigning type to the same value (you can type non-numbers like the letter e and they don't go away when the timer fires).

} else {
node.removeAttribute('type');
}

if (value != null) {
if (type === 'number') {
if (
Expand Down Expand Up @@ -157,6 +178,20 @@ export function updateInput(
if (checked != null && node.checked !== !!checked) {
node.checked = checked;
}

if (
name != null &&
typeof name !== 'function' &&
typeof name !== 'symbol' &&
typeof name !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(name, 'name');
}
node.name = name;
} else {
node.removeAttribute('name');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it better to removeAttribute above instead of = '' so you don't need to do it here?

}
}

export function initInput(
Expand All @@ -166,10 +201,23 @@ export function initInput(
checked: ?boolean,
defaultChecked: ?boolean,
type: ?string,
name: ?string,
isHydrating: boolean,
) {
const node: HTMLInputElement = (element: any);

if (
type != null &&
typeof type !== 'function' &&
typeof type !== 'symbol' &&
typeof type !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(type, 'type');
}
node.type = type;
}

if (value != null || defaultValue != null) {
const isButton = type === 'submit' || type === 'reset';

Expand Down Expand Up @@ -235,10 +283,6 @@ export function initInput(
// will sometimes influence the value of checked (even after detachment).
// Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416
// We need to temporarily unset name to avoid disrupting radio button groups.
const name = node.name;
if (name !== '') {
node.name = '';
}

const checkedOrDefault = checked != null ? checked : defaultChecked;
// TODO: This 'function' or 'symbol' check isn't replicated in other places
Expand Down Expand Up @@ -276,7 +320,16 @@ export function initInput(
node.defaultChecked = !!initialChecked;
}

if (name !== '') {
// Name needs to be set at the end so that it applies atomically to connected radio buttons.
if (
name != null &&
typeof name !== 'function' &&
typeof name !== 'symbol' &&
typeof name !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(name, 'name');
}
node.name = name;
}
}
Expand All @@ -291,6 +344,7 @@ export function restoreControlledInputState(element: Element, props: Object) {
props.checked,
props.defaultChecked,
props.type,
props.name,
);
const name = props.name;
if (props.type === 'radio' && name != null) {
Expand Down Expand Up @@ -347,6 +401,7 @@ export function restoreControlledInputState(element: Element, props: Object) {
otherProps.checked,
otherProps.defaultChecked,
otherProps.type,
otherProps.name,
);
}
}
Expand Down
Loading