main
js 243 lines 7.3 KB
Raw
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 import {queueChangeEvent} from '../events/ReactDOMEventReplaying';
16
17 let didWarnValueDefaultValue;
18
19 if (__DEV__) {
20 didWarnValueDefaultValue = false;
21 }
22
23 function getDeclarationErrorAddendum() {
24 const ownerName = getCurrentFiberOwnerNameInDevOrNull();
25 if (ownerName) {
26 return '\n\nCheck the render method of `' + ownerName + '`.';
27 }
28 return '';
29 }
30
31 const valuePropNames = ['value', 'defaultValue'];
32
33 /**
34 * Validation function for `value` and `defaultValue`.
35 */
36 function checkSelectPropTypes(props: any) {
37 if (__DEV__) {
38 for (let i = 0; i < valuePropNames.length; i++) {
39 const propName = valuePropNames[i];
40 if (props[propName] == null) {
41 continue;
42 }
43 const propNameIsArray = isArray(props[propName]);
44 if (props.multiple && !propNameIsArray) {
45 console.error(
46 'The `%s` prop supplied to <select> must be an array if ' +
47 '`multiple` is true.%s',
48 propName,
49 getDeclarationErrorAddendum(),
50 );
51 } else if (!props.multiple && propNameIsArray) {
52 console.error(
53 'The `%s` prop supplied to <select> must be a scalar ' +
54 'value if `multiple` is false.%s',
55 propName,
56 getDeclarationErrorAddendum(),
57 );
58 }
59 }
60 }
61 }
62
63 function updateOptions(
64 node: HTMLSelectElement,
65 multiple: boolean,
66 propValue: any,
67 setDefaultSelected: boolean,
68 ) {
69 const options: HTMLOptionsCollection = node.options;
70
71 if (multiple) {
72 const selectedValues = propValue as Array<string>;
73 const selectedValue: {[string]: boolean} = {};
74 for (let i = 0; i < selectedValues.length; i++) {
75 // Prefix to avoid chaos with special keys.
76 selectedValue['$' + selectedValues[i]] = true;
77 }
78 for (let i = 0; i < options.length; i++) {
79 const selected = selectedValue.hasOwnProperty('$' + options[i].value);
80 if (options[i].selected !== selected) {
81 options[i].selected = selected;
82 }
83 if (selected && setDefaultSelected) {
84 options[i].defaultSelected = true;
85 }
86 }
87 } else {
88 // Do not set `select.value` as exact behavior isn't consistent across all
89 // browsers for all cases.
90 const selectedValue = toString(getToStringValue(propValue));
91 let defaultSelected = null;
92 for (let i = 0; i < options.length; i++) {
93 if (options[i].value === selectedValue) {
94 options[i].selected = true;
95 if (setDefaultSelected) {
96 options[i].defaultSelected = true;
97 }
98 return;
99 }
100 if (defaultSelected === null && !options[i].disabled) {
101 defaultSelected = options[i];
102 }
103 }
104 if (defaultSelected !== null) {
105 defaultSelected.selected = true;
106 }
107 }
108 }
109
110 /**
111 * Implements a <select> host component that allows optionally setting the
112 * props `value` and `defaultValue`. If `multiple` is false, the prop must be a
113 * stringable. If `multiple` is true, the prop must be an array of stringables.
114 *
115 * If `value` is not supplied (or null/undefined), user actions that change the
116 * selected option will trigger updates to the rendered options.
117 *
118 * If it is supplied (and not null/undefined), the rendered options will not
119 * update in response to user actions. Instead, the `value` prop must change in
120 * order for the rendered options to update.
121 *
122 * If `defaultValue` is provided, any options with the supplied values will be
123 * selected.
124 */
125
126 export function validateSelectProps(element: Element, props: Object) {
127 if (__DEV__) {
128 checkSelectPropTypes(props);
129 if (
130 props.value !== undefined &&
131 props.defaultValue !== undefined &&
132 !didWarnValueDefaultValue
133 ) {
134 console.error(
135 'Select elements must be either controlled or uncontrolled ' +
136 '(specify either the value prop, or the defaultValue prop, but not ' +
137 'both). Decide between using a controlled or uncontrolled select ' +
138 'element and remove one of these props. More info: ' +
139 'https://react.dev/link/controlled-components',
140 );
141 didWarnValueDefaultValue = true;
142 }
143 }
144 }
145
146 export function initSelect(
147 element: Element,
148 value: ?string,
149 defaultValue: ?string,
150 multiple: ?boolean,
151 ) {
152 const node: HTMLSelectElement = element as any;
153 node.multiple = !!multiple;
154 if (value != null) {
155 updateOptions(node, !!multiple, value, false);
156 } else if (defaultValue != null) {
157 updateOptions(node, !!multiple, defaultValue, true);
158 }
159 }
160
161 export function hydrateSelect(
162 element: Element,
163 value: ?string,
164 defaultValue: ?string,
165 multiple: ?boolean,
166 ): void {
167 const node: HTMLSelectElement = element as any;
168 const options: HTMLOptionsCollection = node.options;
169
170 const propValue: any = value != null ? value : defaultValue;
171
172 let changed = false;
173
174 if (multiple) {
175 const selectedValues = propValue as ?Array<string>;
176 const selectedValue: {[string]: boolean} = {};
177 if (selectedValues != null) {
178 for (let i = 0; i < selectedValues.length; i++) {
179 // Prefix to avoid chaos with special keys.
180 selectedValue['$' + selectedValues[i]] = true;
181 }
182 }
183 for (let i = 0; i < options.length; i++) {
184 const expectedSelected = selectedValue.hasOwnProperty(
185 '$' + options[i].value,
186 );
187 if (options[i].selected !== expectedSelected) {
188 changed = true;
189 break;
190 }
191 }
192 } else {
193 let selectedValue =
194 propValue == null ? null : toString(getToStringValue(propValue));
195 for (let i = 0; i < options.length; i++) {
196 if (selectedValue == null && !options[i].disabled) {
197 // We expect the first non-disabled option to be selected if the selected is null.
198 selectedValue = options[i].value;
199 }
200 const expectedSelected = options[i].value === selectedValue;
201 if (options[i].selected !== expectedSelected) {
202 changed = true;
203 break;
204 }
205 }
206 }
207 if (changed) {
208 // If the current selection is different than our initial that suggests that the user
209 // changed it before hydration. Queue a replay of the change event.
210 queueChangeEvent(node);
211 }
212 }
213
214 export function updateSelect(
215 element: Element,
216 value: ?string,
217 defaultValue: ?string,
218 multiple: ?boolean,
219 wasMultiple: ?boolean,
220 ) {
221 const node: HTMLSelectElement = element as any;
222
223 if (value != null) {
224 updateOptions(node, !!multiple, value, false);
225 } else if (!!wasMultiple !== !!multiple) {
226 // For simplicity, reapply `defaultValue` if `multiple` is toggled.
227 if (defaultValue != null) {
228 updateOptions(node, !!multiple, defaultValue, true);
229 } else {
230 // Revert the select back to its default unselected state.
231 updateOptions(node, !!multiple, multiple ? [] : '', false);
232 }
233 }
234 }
235
236 export function restoreControlledSelectState(element: Element, props: Object) {
237 const node: HTMLSelectElement = element as any;
238 const value = props.value;
239
240 if (value != null) {
241 updateOptions(node, !!props.multiple, value, false);
242 }
243 }