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 {getToStringValue, toString} from './ToStringValue';
    
  14. import isArray from 'shared/isArray';
    
  15. 
    
  16. let didWarnValueDefaultValue;
    
  17. 
    
  18. if (__DEV__) {
    
  19.   didWarnValueDefaultValue = false;
    
  20. }
    
  21. 
    
  22. function getDeclarationErrorAddendum() {
    
  23.   const ownerName = getCurrentFiberOwnerNameInDevOrNull();
    
  24.   if (ownerName) {
    
  25.     return '\n\nCheck the render method of `' + ownerName + '`.';
    
  26.   }
    
  27.   return '';
    
  28. }
    
  29. 
    
  30. const valuePropNames = ['value', 'defaultValue'];
    
  31. 
    
  32. /**
    
  33.  * Validation function for `value` and `defaultValue`.
    
  34.  */
    
  35. function checkSelectPropTypes(props: any) {
    
  36.   if (__DEV__) {
    
  37.     for (let i = 0; i < valuePropNames.length; i++) {
    
  38.       const propName = valuePropNames[i];
    
  39.       if (props[propName] == null) {
    
  40.         continue;
    
  41.       }
    
  42.       const propNameIsArray = isArray(props[propName]);
    
  43.       if (props.multiple && !propNameIsArray) {
    
  44.         console.error(
    
  45.           'The `%s` prop supplied to <select> must be an array if ' +
    
  46.             '`multiple` is true.%s',
    
  47.           propName,
    
  48.           getDeclarationErrorAddendum(),
    
  49.         );
    
  50.       } else if (!props.multiple && propNameIsArray) {
    
  51.         console.error(
    
  52.           'The `%s` prop supplied to <select> must be a scalar ' +
    
  53.             'value if `multiple` is false.%s',
    
  54.           propName,
    
  55.           getDeclarationErrorAddendum(),
    
  56.         );
    
  57.       }
    
  58.     }
    
  59.   }
    
  60. }
    
  61. 
    
  62. function updateOptions(
    
  63.   node: HTMLSelectElement,
    
  64.   multiple: boolean,
    
  65.   propValue: any,
    
  66.   setDefaultSelected: boolean,
    
  67. ) {
    
  68.   const options: HTMLOptionsCollection = node.options;
    
  69. 
    
  70.   if (multiple) {
    
  71.     const selectedValues = (propValue: Array<string>);
    
  72.     const selectedValue: {[string]: boolean} = {};
    
  73.     for (let i = 0; i < selectedValues.length; i++) {
    
  74.       // Prefix to avoid chaos with special keys.
    
  75.       selectedValue['$' + selectedValues[i]] = true;
    
  76.     }
    
  77.     for (let i = 0; i < options.length; i++) {
    
  78.       const selected = selectedValue.hasOwnProperty('$' + options[i].value);
    
  79.       if (options[i].selected !== selected) {
    
  80.         options[i].selected = selected;
    
  81.       }
    
  82.       if (selected && setDefaultSelected) {
    
  83.         options[i].defaultSelected = true;
    
  84.       }
    
  85.     }
    
  86.   } else {
    
  87.     // Do not set `select.value` as exact behavior isn't consistent across all
    
  88.     // browsers for all cases.
    
  89.     const selectedValue = toString(getToStringValue((propValue: any)));
    
  90.     let defaultSelected = null;
    
  91.     for (let i = 0; i < options.length; i++) {
    
  92.       if (options[i].value === selectedValue) {
    
  93.         options[i].selected = true;
    
  94.         if (setDefaultSelected) {
    
  95.           options[i].defaultSelected = true;
    
  96.         }
    
  97.         return;
    
  98.       }
    
  99.       if (defaultSelected === null && !options[i].disabled) {
    
  100.         defaultSelected = options[i];
    
  101.       }
    
  102.     }
    
  103.     if (defaultSelected !== null) {
    
  104.       defaultSelected.selected = true;
    
  105.     }
    
  106.   }
    
  107. }
    
  108. 
    
  109. /**
    
  110.  * Implements a <select> host component that allows optionally setting the
    
  111.  * props `value` and `defaultValue`. If `multiple` is false, the prop must be a
    
  112.  * stringable. If `multiple` is true, the prop must be an array of stringables.
    
  113.  *
    
  114.  * If `value` is not supplied (or null/undefined), user actions that change the
    
  115.  * selected option will trigger updates to the rendered options.
    
  116.  *
    
  117.  * If it is supplied (and not null/undefined), the rendered options will not
    
  118.  * update in response to user actions. Instead, the `value` prop must change in
    
  119.  * order for the rendered options to update.
    
  120.  *
    
  121.  * If `defaultValue` is provided, any options with the supplied values will be
    
  122.  * selected.
    
  123.  */
    
  124. 
    
  125. export function validateSelectProps(element: Element, props: Object) {
    
  126.   if (__DEV__) {
    
  127.     checkSelectPropTypes(props);
    
  128.     if (
    
  129.       props.value !== undefined &&
    
  130.       props.defaultValue !== undefined &&
    
  131.       !didWarnValueDefaultValue
    
  132.     ) {
    
  133.       console.error(
    
  134.         'Select elements must be either controlled or uncontrolled ' +
    
  135.           '(specify either the value prop, or the defaultValue prop, but not ' +
    
  136.           'both). Decide between using a controlled or uncontrolled select ' +
    
  137.           'element and remove one of these props. More info: ' +
    
  138.           'https://reactjs.org/link/controlled-components',
    
  139.       );
    
  140.       didWarnValueDefaultValue = true;
    
  141.     }
    
  142.   }
    
  143. }
    
  144. 
    
  145. export function initSelect(
    
  146.   element: Element,
    
  147.   value: ?string,
    
  148.   defaultValue: ?string,
    
  149.   multiple: ?boolean,
    
  150. ) {
    
  151.   const node: HTMLSelectElement = (element: any);
    
  152.   node.multiple = !!multiple;
    
  153.   if (value != null) {
    
  154.     updateOptions(node, !!multiple, value, false);
    
  155.   } else if (defaultValue != null) {
    
  156.     updateOptions(node, !!multiple, defaultValue, true);
    
  157.   }
    
  158. }
    
  159. 
    
  160. export function updateSelect(
    
  161.   element: Element,
    
  162.   value: ?string,
    
  163.   defaultValue: ?string,
    
  164.   multiple: ?boolean,
    
  165.   wasMultiple: ?boolean,
    
  166. ) {
    
  167.   const node: HTMLSelectElement = (element: any);
    
  168. 
    
  169.   if (value != null) {
    
  170.     updateOptions(node, !!multiple, value, false);
    
  171.   } else if (!!wasMultiple !== !!multiple) {
    
  172.     // For simplicity, reapply `defaultValue` if `multiple` is toggled.
    
  173.     if (defaultValue != null) {
    
  174.       updateOptions(node, !!multiple, defaultValue, true);
    
  175.     } else {
    
  176.       // Revert the select back to its default unselected state.
    
  177.       updateOptions(node, !!multiple, multiple ? [] : '', false);
    
  178.     }
    
  179.   }
    
  180. }
    
  181. 
    
  182. export function restoreControlledSelectState(element: Element, props: Object) {
    
  183.   const node: HTMLSelectElement = (element: any);
    
  184.   const value = props.value;
    
  185. 
    
  186.   if (value != null) {
    
  187.     updateOptions(node, !!props.multiple, value, false);
    
  188.   }
    
  189. }