1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. // TODO: direct imports like some-package/src/* are bad. Fix me.
    
  11. import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber';
    
  12. 
    
  13. import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
    
  14. import {getToStringValue, toString} from './ToStringValue';
    
  15. import {updateValueIfChanged} from './inputValueTracking';
    
  16. import getActiveElement from './getActiveElement';
    
  17. import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
    
  18. import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
    
  19. 
    
  20. import type {ToStringValue} from './ToStringValue';
    
  21. import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes';
    
  22. 
    
  23. let didWarnValueDefaultValue = false;
    
  24. let didWarnCheckedDefaultChecked = false;
    
  25. 
    
  26. /**
    
  27.  * Implements an <input> host component that allows setting these optional
    
  28.  * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
    
  29.  *
    
  30.  * If `checked` or `value` are not supplied (or null/undefined), user actions
    
  31.  * that affect the checked state or value will trigger updates to the element.
    
  32.  *
    
  33.  * If they are supplied (and not null/undefined), the rendered element will not
    
  34.  * trigger updates to the element. Instead, the props must change in order for
    
  35.  * the rendered element to be updated.
    
  36.  *
    
  37.  * The rendered element will be initialized as unchecked (or `defaultChecked`)
    
  38.  * with an empty value (or `defaultValue`).
    
  39.  *
    
  40.  * See http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
    
  41.  */
    
  42. 
    
  43. export function validateInputProps(element: Element, props: Object) {
    
  44.   if (__DEV__) {
    
  45.     // Normally we check for undefined and null the same, but explicitly specifying both
    
  46.     // properties, at all is probably worth warning for. We could move this either direction
    
  47.     // and just make it ok to pass null or just check hasOwnProperty.
    
  48.     if (
    
  49.       props.checked !== undefined &&
    
  50.       props.defaultChecked !== undefined &&
    
  51.       !didWarnCheckedDefaultChecked
    
  52.     ) {
    
  53.       console.error(
    
  54.         '%s contains an input of type %s with both checked and defaultChecked props. ' +
    
  55.           'Input elements must be either controlled or uncontrolled ' +
    
  56.           '(specify either the checked prop, or the defaultChecked prop, but not ' +
    
  57.           'both). Decide between using a controlled or uncontrolled input ' +
    
  58.           'element and remove one of these props. More info: ' +
    
  59.           'https://reactjs.org/link/controlled-components',
    
  60.         getCurrentFiberOwnerNameInDevOrNull() || 'A component',
    
  61.         props.type,
    
  62.       );
    
  63.       didWarnCheckedDefaultChecked = true;
    
  64.     }
    
  65.     if (
    
  66.       props.value !== undefined &&
    
  67.       props.defaultValue !== undefined &&
    
  68.       !didWarnValueDefaultValue
    
  69.     ) {
    
  70.       console.error(
    
  71.         '%s contains an input of type %s with both value and defaultValue props. ' +
    
  72.           'Input elements must be either controlled or uncontrolled ' +
    
  73.           '(specify either the value prop, or the defaultValue prop, but not ' +
    
  74.           'both). Decide between using a controlled or uncontrolled input ' +
    
  75.           'element and remove one of these props. More info: ' +
    
  76.           'https://reactjs.org/link/controlled-components',
    
  77.         getCurrentFiberOwnerNameInDevOrNull() || 'A component',
    
  78.         props.type,
    
  79.       );
    
  80.       didWarnValueDefaultValue = true;
    
  81.     }
    
  82.   }
    
  83. }
    
  84. 
    
  85. export function updateInput(
    
  86.   element: Element,
    
  87.   value: ?string,
    
  88.   defaultValue: ?string,
    
  89.   lastDefaultValue: ?string,
    
  90.   checked: ?boolean,
    
  91.   defaultChecked: ?boolean,
    
  92.   type: ?string,
    
  93.   name: ?string,
    
  94. ) {
    
  95.   const node: HTMLInputElement = (element: any);
    
  96. 
    
  97.   // Temporarily disconnect the input from any radio buttons.
    
  98.   // Changing the type or name as the same time as changing the checked value
    
  99.   // needs to be atomically applied. We can only ensure that by disconnecting
    
  100.   // the name while do the mutations and then reapply the name after that's done.
    
  101.   node.name = '';
    
  102. 
    
  103.   if (
    
  104.     type != null &&
    
  105.     typeof type !== 'function' &&
    
  106.     typeof type !== 'symbol' &&
    
  107.     typeof type !== 'boolean'
    
  108.   ) {
    
  109.     if (__DEV__) {
    
  110.       checkAttributeStringCoercion(type, 'type');
    
  111.     }
    
  112.     node.type = type;
    
  113.   } else {
    
  114.     node.removeAttribute('type');
    
  115.   }
    
  116. 
    
  117.   if (value != null) {
    
  118.     if (type === 'number') {
    
  119.       if (
    
  120.         // $FlowFixMe[incompatible-type]
    
  121.         (value === 0 && node.value === '') ||
    
  122.         // We explicitly want to coerce to number here if possible.
    
  123.         // eslint-disable-next-line
    
  124.         node.value != (value: any)
    
  125.       ) {
    
  126.         node.value = toString(getToStringValue(value));
    
  127.       }
    
  128.     } else if (node.value !== toString(getToStringValue(value))) {
    
  129.       node.value = toString(getToStringValue(value));
    
  130.     }
    
  131.   } else if (type === 'submit' || type === 'reset') {
    
  132.     // Submit/reset inputs need the attribute removed completely to avoid
    
  133.     // blank-text buttons.
    
  134.     node.removeAttribute('value');
    
  135.   }
    
  136. 
    
  137.   if (disableInputAttributeSyncing) {
    
  138.     // When not syncing the value attribute, React only assigns a new value
    
  139.     // whenever the defaultValue React prop has changed. When not present,
    
  140.     // React does nothing
    
  141.     if (defaultValue != null) {
    
  142.       setDefaultValue(node, type, getToStringValue(defaultValue));
    
  143.     } else if (lastDefaultValue != null) {
    
  144.       node.removeAttribute('value');
    
  145.     }
    
  146.   } else {
    
  147.     // When syncing the value attribute, the value comes from a cascade of
    
  148.     // properties:
    
  149.     //  1. The value React property
    
  150.     //  2. The defaultValue React property
    
  151.     //  3. Otherwise there should be no change
    
  152.     if (value != null) {
    
  153.       setDefaultValue(node, type, getToStringValue(value));
    
  154.     } else if (defaultValue != null) {
    
  155.       setDefaultValue(node, type, getToStringValue(defaultValue));
    
  156.     } else if (lastDefaultValue != null) {
    
  157.       node.removeAttribute('value');
    
  158.     }
    
  159.   }
    
  160. 
    
  161.   if (disableInputAttributeSyncing) {
    
  162.     // When not syncing the checked attribute, the attribute is directly
    
  163.     // controllable from the defaultValue React property. It needs to be
    
  164.     // updated as new props come in.
    
  165.     if (defaultChecked == null) {
    
  166.       node.removeAttribute('checked');
    
  167.     } else {
    
  168.       node.defaultChecked = !!defaultChecked;
    
  169.     }
    
  170.   } else {
    
  171.     // When syncing the checked attribute, it only changes when it needs
    
  172.     // to be removed, such as transitioning from a checkbox into a text input
    
  173.     if (checked == null && defaultChecked != null) {
    
  174.       node.defaultChecked = !!defaultChecked;
    
  175.     }
    
  176.   }
    
  177. 
    
  178.   if (checked != null) {
    
  179.     // Important to set this even if it's not a change in order to update input
    
  180.     // value tracking with radio buttons
    
  181.     // TODO: Should really update input value tracking for the whole radio
    
  182.     // button group in an effect or something (similar to #27024)
    
  183.     node.checked =
    
  184.       checked && typeof checked !== 'function' && typeof checked !== 'symbol';
    
  185.   }
    
  186. 
    
  187.   if (
    
  188.     name != null &&
    
  189.     typeof name !== 'function' &&
    
  190.     typeof name !== 'symbol' &&
    
  191.     typeof name !== 'boolean'
    
  192.   ) {
    
  193.     if (__DEV__) {
    
  194.       checkAttributeStringCoercion(name, 'name');
    
  195.     }
    
  196.     node.name = toString(getToStringValue(name));
    
  197.   } else {
    
  198.     node.removeAttribute('name');
    
  199.   }
    
  200. }
    
  201. 
    
  202. export function initInput(
    
  203.   element: Element,
    
  204.   value: ?string,
    
  205.   defaultValue: ?string,
    
  206.   checked: ?boolean,
    
  207.   defaultChecked: ?boolean,
    
  208.   type: ?string,
    
  209.   name: ?string,
    
  210.   isHydrating: boolean,
    
  211. ) {
    
  212.   const node: HTMLInputElement = (element: any);
    
  213. 
    
  214.   if (
    
  215.     type != null &&
    
  216.     typeof type !== 'function' &&
    
  217.     typeof type !== 'symbol' &&
    
  218.     typeof type !== 'boolean'
    
  219.   ) {
    
  220.     if (__DEV__) {
    
  221.       checkAttributeStringCoercion(type, 'type');
    
  222.     }
    
  223.     node.type = type;
    
  224.   }
    
  225. 
    
  226.   if (value != null || defaultValue != null) {
    
  227.     const isButton = type === 'submit' || type === 'reset';
    
  228. 
    
  229.     // Avoid setting value attribute on submit/reset inputs as it overrides the
    
  230.     // default value provided by the browser. See: #12872
    
  231.     if (isButton && (value === undefined || value === null)) {
    
  232.       return;
    
  233.     }
    
  234. 
    
  235.     const defaultValueStr =
    
  236.       defaultValue != null ? toString(getToStringValue(defaultValue)) : '';
    
  237.     const initialValue =
    
  238.       value != null ? toString(getToStringValue(value)) : defaultValueStr;
    
  239. 
    
  240.     // Do not assign value if it is already set. This prevents user text input
    
  241.     // from being lost during SSR hydration.
    
  242.     if (!isHydrating) {
    
  243.       if (disableInputAttributeSyncing) {
    
  244.         // When not syncing the value attribute, the value property points
    
  245.         // directly to the React prop. Only assign it if it exists.
    
  246.         if (value != null) {
    
  247.           // Always assign on buttons so that it is possible to assign an
    
  248.           // empty string to clear button text.
    
  249.           //
    
  250.           // Otherwise, do not re-assign the value property if is empty. This
    
  251.           // potentially avoids a DOM write and prevents Firefox (~60.0.1) from
    
  252.           // prematurely marking required inputs as invalid. Equality is compared
    
  253.           // to the current value in case the browser provided value is not an
    
  254.           // empty string.
    
  255.           if (isButton || toString(getToStringValue(value)) !== node.value) {
    
  256.             node.value = toString(getToStringValue(value));
    
  257.           }
    
  258.         }
    
  259.       } else {
    
  260.         // When syncing the value attribute, the value property should use
    
  261.         // the wrapperState._initialValue property. This uses:
    
  262.         //
    
  263.         //   1. The value React property when present
    
  264.         //   2. The defaultValue React property when present
    
  265.         //   3. An empty string
    
  266.         if (initialValue !== node.value) {
    
  267.           node.value = initialValue;
    
  268.         }
    
  269.       }
    
  270.     }
    
  271. 
    
  272.     if (disableInputAttributeSyncing) {
    
  273.       // When not syncing the value attribute, assign the value attribute
    
  274.       // directly from the defaultValue React property (when present)
    
  275.       if (defaultValue != null) {
    
  276.         node.defaultValue = defaultValueStr;
    
  277.       }
    
  278.     } else {
    
  279.       // Otherwise, the value attribute is synchronized to the property,
    
  280.       // so we assign defaultValue to the same thing as the value property
    
  281.       // assignment step above.
    
  282.       node.defaultValue = initialValue;
    
  283.     }
    
  284.   }
    
  285. 
    
  286.   // Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug
    
  287.   // this is needed to work around a chrome bug where setting defaultChecked
    
  288.   // will sometimes influence the value of checked (even after detachment).
    
  289.   // Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416
    
  290.   // We need to temporarily unset name to avoid disrupting radio button groups.
    
  291. 
    
  292.   const checkedOrDefault = checked != null ? checked : defaultChecked;
    
  293.   // TODO: This 'function' or 'symbol' check isn't replicated in other places
    
  294.   // so this semantic is inconsistent.
    
  295.   const initialChecked =
    
  296.     typeof checkedOrDefault !== 'function' &&
    
  297.     typeof checkedOrDefault !== 'symbol' &&
    
  298.     !!checkedOrDefault;
    
  299. 
    
  300.   if (isHydrating) {
    
  301.     // Detach .checked from .defaultChecked but leave user input alone
    
  302.     node.checked = node.checked;
    
  303.   } else {
    
  304.     node.checked = !!initialChecked;
    
  305.   }
    
  306. 
    
  307.   if (disableInputAttributeSyncing) {
    
  308.     // Only assign the checked attribute if it is defined. This saves
    
  309.     // a DOM write when controlling the checked attribute isn't needed
    
  310.     // (text inputs, submit/reset)
    
  311.     if (defaultChecked != null) {
    
  312.       node.defaultChecked = !node.defaultChecked;
    
  313.       node.defaultChecked = !!defaultChecked;
    
  314.     }
    
  315.   } else {
    
  316.     // When syncing the checked attribute, both the checked property and
    
  317.     // attribute are assigned at the same time using defaultChecked. This uses:
    
  318.     //
    
  319.     //   1. The checked React property when present
    
  320.     //   2. The defaultChecked React property when present
    
  321.     //   3. Otherwise, false
    
  322.     node.defaultChecked = !node.defaultChecked;
    
  323.     node.defaultChecked = !!initialChecked;
    
  324.   }
    
  325. 
    
  326.   // Name needs to be set at the end so that it applies atomically to connected radio buttons.
    
  327.   if (
    
  328.     name != null &&
    
  329.     typeof name !== 'function' &&
    
  330.     typeof name !== 'symbol' &&
    
  331.     typeof name !== 'boolean'
    
  332.   ) {
    
  333.     if (__DEV__) {
    
  334.       checkAttributeStringCoercion(name, 'name');
    
  335.     }
    
  336.     node.name = name;
    
  337.   }
    
  338. }
    
  339. 
    
  340. export function restoreControlledInputState(element: Element, props: Object) {
    
  341.   const rootNode: HTMLInputElement = (element: any);
    
  342.   updateInput(
    
  343.     rootNode,
    
  344.     props.value,
    
  345.     props.defaultValue,
    
  346.     props.defaultValue,
    
  347.     props.checked,
    
  348.     props.defaultChecked,
    
  349.     props.type,
    
  350.     props.name,
    
  351.   );
    
  352.   const name = props.name;
    
  353.   if (props.type === 'radio' && name != null) {
    
  354.     let queryRoot: Element = rootNode;
    
  355. 
    
  356.     while (queryRoot.parentNode) {
    
  357.       queryRoot = ((queryRoot.parentNode: any): Element);
    
  358.     }
    
  359. 
    
  360.     // If `rootNode.form` was non-null, then we could try `form.elements`,
    
  361.     // but that sometimes behaves strangely in IE8. We could also try using
    
  362.     // `form.getElementsByName`, but that will only return direct children
    
  363.     // and won't include inputs that use the HTML5 `form=` attribute. Since
    
  364.     // the input might not even be in a form. It might not even be in the
    
  365.     // document. Let's just use the local `querySelectorAll` to ensure we don't
    
  366.     // miss anything.
    
  367.     if (__DEV__) {
    
  368.       checkAttributeStringCoercion(name, 'name');
    
  369.     }
    
  370.     const group = queryRoot.querySelectorAll(
    
  371.       'input[name="' +
    
  372.         escapeSelectorAttributeValueInsideDoubleQuotes('' + name) +
    
  373.         '"][type="radio"]',
    
  374.     );
    
  375. 
    
  376.     for (let i = 0; i < group.length; i++) {
    
  377.       const otherNode = ((group[i]: any): HTMLInputElement);
    
  378.       if (otherNode === rootNode || otherNode.form !== rootNode.form) {
    
  379.         continue;
    
  380.       }
    
  381.       // This will throw if radio buttons rendered by different copies of React
    
  382.       // and the same name are rendered into the same form (same as #1939).
    
  383.       // That's probably okay; we don't support it just as we don't support
    
  384.       // mixing React radio buttons with non-React ones.
    
  385.       const otherProps: any = getFiberCurrentPropsFromNode(otherNode);
    
  386. 
    
  387.       if (!otherProps) {
    
  388.         throw new Error(
    
  389.           'ReactDOMInput: Mixing React and non-React radio inputs with the ' +
    
  390.             'same `name` is not supported.',
    
  391.         );
    
  392.       }
    
  393. 
    
  394.       // If this is a controlled radio button group, forcing the input that
    
  395.       // was previously checked to update will cause it to be come re-checked
    
  396.       // as appropriate.
    
  397.       updateInput(
    
  398.         otherNode,
    
  399.         otherProps.value,
    
  400.         otherProps.defaultValue,
    
  401.         otherProps.defaultValue,
    
  402.         otherProps.checked,
    
  403.         otherProps.defaultChecked,
    
  404.         otherProps.type,
    
  405.         otherProps.name,
    
  406.       );
    
  407.     }
    
  408. 
    
  409.     // If any updateInput() call set .checked to true, an input in this group
    
  410.     // (often, `rootNode` itself) may have become unchecked
    
  411.     for (let i = 0; i < group.length; i++) {
    
  412.       const otherNode = ((group[i]: any): HTMLInputElement);
    
  413.       if (otherNode.form !== rootNode.form) {
    
  414.         continue;
    
  415.       }
    
  416.       updateValueIfChanged(otherNode);
    
  417.     }
    
  418.   }
    
  419. }
    
  420. 
    
  421. // In Chrome, assigning defaultValue to certain input types triggers input validation.
    
  422. // For number inputs, the display value loses trailing decimal points. For email inputs,
    
  423. // Chrome raises "The specified value <x> is not a valid email address".
    
  424. //
    
  425. // Here we check to see if the defaultValue has actually changed, avoiding these problems
    
  426. // when the user is inputting text
    
  427. //
    
  428. // https://github.com/facebook/react/issues/7253
    
  429. export function setDefaultValue(
    
  430.   node: HTMLInputElement,
    
  431.   type: ?string,
    
  432.   value: ToStringValue,
    
  433. ) {
    
  434.   if (
    
  435.     // Focused number inputs synchronize on blur. See ChangeEventPlugin.js
    
  436.     type !== 'number' ||
    
  437.     getActiveElement(node.ownerDocument) !== node
    
  438.   ) {
    
  439.     if (node.defaultValue !== toString(value)) {
    
  440.       node.defaultValue = toString(value);
    
  441.     }
    
  442.   }
    
  443. }