1 | import _extends from "@babel/runtime/helpers/esm/extends";
|
2 | import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
|
3 | import { formatMuiErrorMessage as _formatMuiErrorMessage } from "@material-ui/utils";
|
4 | import * as React from 'react';
|
5 | import { isFragment } from 'react-is';
|
6 | import PropTypes from 'prop-types';
|
7 | import clsx from 'clsx';
|
8 | import ownerDocument from '../utils/ownerDocument';
|
9 | import capitalize from '../utils/capitalize';
|
10 | import { refType } from '@material-ui/utils';
|
11 | import Menu from '../Menu/Menu';
|
12 | import { isFilled } from '../InputBase/utils';
|
13 | import useForkRef from '../utils/useForkRef';
|
14 | import useControlled from '../utils/useControlled';
|
15 |
|
16 | function areEqualValues(a, b) {
|
17 | if (typeof b === 'object' && b !== null) {
|
18 | return a === b;
|
19 | }
|
20 |
|
21 | return String(a) === String(b);
|
22 | }
|
23 |
|
24 | function isEmpty(display) {
|
25 | return display == null || typeof display === 'string' && !display.trim();
|
26 | }
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 | const SelectInput = React.forwardRef(function SelectInput(props, ref) {
|
33 | const {
|
34 | 'aria-label': ariaLabel,
|
35 | autoFocus,
|
36 | autoWidth,
|
37 | children,
|
38 | classes,
|
39 | className,
|
40 | defaultValue,
|
41 | disabled,
|
42 | displayEmpty,
|
43 | IconComponent,
|
44 | inputRef: inputRefProp,
|
45 | labelId,
|
46 | MenuProps = {},
|
47 | multiple,
|
48 | name,
|
49 | onBlur,
|
50 | onChange,
|
51 | onClose,
|
52 | onFocus,
|
53 | onOpen,
|
54 | open: openProp,
|
55 | readOnly,
|
56 | renderValue,
|
57 | SelectDisplayProps = {},
|
58 | tabIndex: tabIndexProp,
|
59 | value: valueProp,
|
60 | variant = 'standard'
|
61 | } = props,
|
62 | other = _objectWithoutPropertiesLoose(props, ["aria-label", "autoFocus", "autoWidth", "children", "classes", "className", "defaultValue", "disabled", "displayEmpty", "IconComponent", "inputRef", "labelId", "MenuProps", "multiple", "name", "onBlur", "onChange", "onClose", "onFocus", "onOpen", "open", "readOnly", "renderValue", "SelectDisplayProps", "tabIndex", "type", "value", "variant"]);
|
63 |
|
64 | const [value, setValue] = useControlled({
|
65 | controlled: valueProp,
|
66 | default: defaultValue,
|
67 | name: 'Select'
|
68 | });
|
69 | const inputRef = React.useRef(null);
|
70 | const [displayNode, setDisplayNode] = React.useState(null);
|
71 | const {
|
72 | current: isOpenControlled
|
73 | } = React.useRef(openProp != null);
|
74 | const [menuMinWidthState, setMenuMinWidthState] = React.useState();
|
75 | const [openState, setOpenState] = React.useState(false);
|
76 | const handleRef = useForkRef(ref, inputRefProp);
|
77 | React.useImperativeHandle(handleRef, () => ({
|
78 | focus: () => {
|
79 | displayNode.focus();
|
80 | },
|
81 | node: inputRef.current,
|
82 | value
|
83 | }), [displayNode, value]);
|
84 | React.useEffect(() => {
|
85 | if (autoFocus && displayNode) {
|
86 | displayNode.focus();
|
87 | }
|
88 | }, [autoFocus, displayNode]);
|
89 | React.useEffect(() => {
|
90 | if (displayNode) {
|
91 | const label = ownerDocument(displayNode).getElementById(labelId);
|
92 |
|
93 | if (label) {
|
94 | const handler = () => {
|
95 | if (getSelection().isCollapsed) {
|
96 | displayNode.focus();
|
97 | }
|
98 | };
|
99 |
|
100 | label.addEventListener('click', handler);
|
101 | return () => {
|
102 | label.removeEventListener('click', handler);
|
103 | };
|
104 | }
|
105 | }
|
106 |
|
107 | return undefined;
|
108 | }, [labelId, displayNode]);
|
109 |
|
110 | const update = (open, event) => {
|
111 | if (open) {
|
112 | if (onOpen) {
|
113 | onOpen(event);
|
114 | }
|
115 | } else if (onClose) {
|
116 | onClose(event);
|
117 | }
|
118 |
|
119 | if (!isOpenControlled) {
|
120 | setMenuMinWidthState(autoWidth ? null : displayNode.clientWidth);
|
121 | setOpenState(open);
|
122 | }
|
123 | };
|
124 |
|
125 | const handleMouseDown = event => {
|
126 |
|
127 | if (event.button !== 0) {
|
128 | return;
|
129 | }
|
130 |
|
131 |
|
132 | event.preventDefault();
|
133 | displayNode.focus();
|
134 | update(true, event);
|
135 | };
|
136 |
|
137 | const handleClose = event => {
|
138 | update(false, event);
|
139 | };
|
140 |
|
141 | const childrenArray = React.Children.toArray(children);
|
142 |
|
143 | const handleChange = event => {
|
144 | const index = childrenArray.map(child => child.props.value).indexOf(event.target.value);
|
145 |
|
146 | if (index === -1) {
|
147 | return;
|
148 | }
|
149 |
|
150 | const child = childrenArray[index];
|
151 | setValue(child.props.value);
|
152 |
|
153 | if (onChange) {
|
154 | onChange(event, child);
|
155 | }
|
156 | };
|
157 |
|
158 | const handleItemClick = child => event => {
|
159 | if (!multiple) {
|
160 | update(false, event);
|
161 | }
|
162 |
|
163 | let newValue;
|
164 |
|
165 | if (multiple) {
|
166 | newValue = Array.isArray(value) ? value.slice() : [];
|
167 | const itemIndex = value.indexOf(child.props.value);
|
168 |
|
169 | if (itemIndex === -1) {
|
170 | newValue.push(child.props.value);
|
171 | } else {
|
172 | newValue.splice(itemIndex, 1);
|
173 | }
|
174 | } else {
|
175 | newValue = child.props.value;
|
176 | }
|
177 |
|
178 | if (child.props.onClick) {
|
179 | child.props.onClick(event);
|
180 | }
|
181 |
|
182 | if (value === newValue) {
|
183 | return;
|
184 | }
|
185 |
|
186 | setValue(newValue);
|
187 |
|
188 | if (onChange) {
|
189 | event.persist();
|
190 |
|
191 | Object.defineProperty(event, 'target', {
|
192 | writable: true,
|
193 | value: {
|
194 | value: newValue,
|
195 | name
|
196 | }
|
197 | });
|
198 | onChange(event, child);
|
199 | }
|
200 | };
|
201 |
|
202 | const handleKeyDown = event => {
|
203 | if (!readOnly) {
|
204 | const validKeys = [' ', 'ArrowUp', 'ArrowDown',
|
205 |
|
206 | 'Enter'];
|
207 |
|
208 | if (validKeys.indexOf(event.key) !== -1) {
|
209 | event.preventDefault();
|
210 | update(true, event);
|
211 | }
|
212 | }
|
213 | };
|
214 |
|
215 | const open = displayNode !== null && (isOpenControlled ? openProp : openState);
|
216 |
|
217 | const handleBlur = event => {
|
218 |
|
219 | if (!open && onBlur) {
|
220 | event.persist();
|
221 |
|
222 | Object.defineProperty(event, 'target', {
|
223 | writable: true,
|
224 | value: {
|
225 | value,
|
226 | name
|
227 | }
|
228 | });
|
229 | onBlur(event);
|
230 | }
|
231 | };
|
232 |
|
233 | delete other['aria-invalid'];
|
234 | let display;
|
235 | let displaySingle;
|
236 | const displayMultiple = [];
|
237 | let computeDisplay = false;
|
238 | let foundMatch = false;
|
239 |
|
240 | if (isFilled({
|
241 | value
|
242 | }) || displayEmpty) {
|
243 | if (renderValue) {
|
244 | display = renderValue(value);
|
245 | } else {
|
246 | computeDisplay = true;
|
247 | }
|
248 | }
|
249 |
|
250 | const items = childrenArray.map(child => {
|
251 | if (! React.isValidElement(child)) {
|
252 | return null;
|
253 | }
|
254 |
|
255 | if (process.env.NODE_ENV !== 'production') {
|
256 | if (isFragment(child)) {
|
257 | console.error(["Material-UI: The Select component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n'));
|
258 | }
|
259 | }
|
260 |
|
261 | let selected;
|
262 |
|
263 | if (multiple) {
|
264 | if (!Array.isArray(value)) {
|
265 | throw new Error(process.env.NODE_ENV !== "production" ? `Material-UI: The \`value\` prop must be an array when using the \`Select\` component with \`multiple\`.` : _formatMuiErrorMessage(2));
|
266 | }
|
267 |
|
268 | selected = value.some(v => areEqualValues(v, child.props.value));
|
269 |
|
270 | if (selected && computeDisplay) {
|
271 | displayMultiple.push(child.props.children);
|
272 | }
|
273 | } else {
|
274 | selected = areEqualValues(value, child.props.value);
|
275 |
|
276 | if (selected && computeDisplay) {
|
277 | displaySingle = child.props.children;
|
278 | }
|
279 | }
|
280 |
|
281 | if (selected) {
|
282 | foundMatch = true;
|
283 | }
|
284 |
|
285 | return React.cloneElement(child, {
|
286 | 'aria-selected': selected ? 'true' : undefined,
|
287 | onClick: handleItemClick(child),
|
288 | onKeyUp: event => {
|
289 | if (event.key === ' ') {
|
290 |
|
291 |
|
292 |
|
293 | event.preventDefault();
|
294 | }
|
295 |
|
296 | if (child.props.onKeyUp) {
|
297 | child.props.onKeyUp(event);
|
298 | }
|
299 | },
|
300 | role: 'option',
|
301 | selected,
|
302 | value: undefined,
|
303 |
|
304 | 'data-value': child.props.value
|
305 |
|
306 | });
|
307 | });
|
308 |
|
309 | if (process.env.NODE_ENV !== 'production') {
|
310 |
|
311 | React.useEffect(() => {
|
312 | if (!foundMatch && !multiple && value !== '') {
|
313 | const values = childrenArray.map(child => child.props.value);
|
314 | console.warn([`Material-UI: You have provided an out-of-range value \`${value}\` for the select ${name ? `(name="${name}") ` : ''}component.`, "Consider providing a value that matches one of the available options or ''.", `The available values are ${values.filter(x => x != null).map(x => `\`${x}\``).join(', ') || '""'}.`].join('\n'));
|
315 | }
|
316 | }, [foundMatch, childrenArray, multiple, name, value]);
|
317 | }
|
318 |
|
319 | if (computeDisplay) {
|
320 | display = multiple ? displayMultiple.join(', ') : displaySingle;
|
321 | }
|
322 |
|
323 |
|
324 | let menuMinWidth = menuMinWidthState;
|
325 |
|
326 | if (!autoWidth && isOpenControlled && displayNode) {
|
327 | menuMinWidth = displayNode.clientWidth;
|
328 | }
|
329 |
|
330 | let tabIndex;
|
331 |
|
332 | if (typeof tabIndexProp !== 'undefined') {
|
333 | tabIndex = tabIndexProp;
|
334 | } else {
|
335 | tabIndex = disabled ? null : 0;
|
336 | }
|
337 |
|
338 | const buttonId = SelectDisplayProps.id || (name ? `mui-component-select-${name}` : undefined);
|
339 | return React.createElement(React.Fragment, null, React.createElement("div", _extends({
|
340 | className: clsx(classes.root,
|
341 | classes.select, classes.selectMenu, classes[variant], className, disabled && classes.disabled),
|
342 | ref: setDisplayNode,
|
343 | tabIndex: tabIndex,
|
344 | role: "button",
|
345 | "aria-disabled": disabled ? 'true' : undefined,
|
346 | "aria-expanded": open ? 'true' : undefined,
|
347 | "aria-haspopup": "listbox",
|
348 | "aria-label": ariaLabel,
|
349 | "aria-labelledby": [labelId, buttonId].filter(Boolean).join(' ') || undefined,
|
350 | onKeyDown: handleKeyDown,
|
351 | onMouseDown: disabled || readOnly ? null : handleMouseDown,
|
352 | onBlur: handleBlur,
|
353 | onFocus: onFocus
|
354 | }, SelectDisplayProps, {
|
355 |
|
356 | id: buttonId
|
357 | }), isEmpty(display) ?
|
358 |
|
359 |
|
360 | React.createElement("span", {
|
361 | dangerouslySetInnerHTML: {
|
362 | __html: '​'
|
363 | }
|
364 | }) : display), React.createElement("input", _extends({
|
365 | value: Array.isArray(value) ? value.join(',') : value,
|
366 | name: name,
|
367 | ref: inputRef,
|
368 | "aria-hidden": true,
|
369 | onChange: handleChange,
|
370 | tabIndex: -1,
|
371 | className: classes.nativeInput,
|
372 | autoFocus: autoFocus
|
373 | }, other)), React.createElement(IconComponent, {
|
374 | className: clsx(classes.icon, classes[`icon${capitalize(variant)}`], open && classes.iconOpen, disabled && classes.disabled)
|
375 | }), React.createElement(Menu, _extends({
|
376 | id: `menu-${name || ''}`,
|
377 | anchorEl: displayNode,
|
378 | open: open,
|
379 | onClose: handleClose
|
380 | }, MenuProps, {
|
381 | MenuListProps: _extends({
|
382 | 'aria-labelledby': labelId,
|
383 | role: 'listbox',
|
384 | disableListWrap: true
|
385 | }, MenuProps.MenuListProps),
|
386 | PaperProps: _extends({}, MenuProps.PaperProps, {
|
387 | style: _extends({
|
388 | minWidth: menuMinWidth
|
389 | }, MenuProps.PaperProps != null ? MenuProps.PaperProps.style : null)
|
390 | })
|
391 | }), items));
|
392 | });
|
393 | process.env.NODE_ENV !== "production" ? SelectInput.propTypes = {
|
394 | |
395 |
|
396 |
|
397 | 'aria-label': PropTypes.string,
|
398 |
|
399 | |
400 |
|
401 |
|
402 | autoFocus: PropTypes.bool,
|
403 |
|
404 | |
405 |
|
406 |
|
407 |
|
408 | autoWidth: PropTypes.bool,
|
409 |
|
410 | |
411 |
|
412 |
|
413 |
|
414 | children: PropTypes.node,
|
415 |
|
416 | |
417 |
|
418 |
|
419 |
|
420 | classes: PropTypes.object.isRequired,
|
421 |
|
422 | |
423 |
|
424 |
|
425 | className: PropTypes.string,
|
426 |
|
427 | |
428 |
|
429 |
|
430 | defaultValue: PropTypes.any,
|
431 |
|
432 | |
433 |
|
434 |
|
435 | disabled: PropTypes.bool,
|
436 |
|
437 | |
438 |
|
439 |
|
440 | displayEmpty: PropTypes.bool,
|
441 |
|
442 | |
443 |
|
444 |
|
445 | IconComponent: PropTypes.elementType.isRequired,
|
446 |
|
447 | |
448 |
|
449 |
|
450 |
|
451 | inputRef: refType,
|
452 |
|
453 | |
454 |
|
455 |
|
456 |
|
457 | labelId: PropTypes.string,
|
458 |
|
459 | |
460 |
|
461 |
|
462 | MenuProps: PropTypes.object,
|
463 |
|
464 | |
465 |
|
466 |
|
467 | multiple: PropTypes.bool,
|
468 |
|
469 | |
470 |
|
471 |
|
472 | name: PropTypes.string,
|
473 |
|
474 | |
475 |
|
476 |
|
477 | onBlur: PropTypes.func,
|
478 |
|
479 | |
480 |
|
481 |
|
482 |
|
483 |
|
484 |
|
485 |
|
486 | onChange: PropTypes.func,
|
487 |
|
488 | |
489 |
|
490 |
|
491 |
|
492 |
|
493 |
|
494 | onClose: PropTypes.func,
|
495 |
|
496 | |
497 |
|
498 |
|
499 | onFocus: PropTypes.func,
|
500 |
|
501 | |
502 |
|
503 |
|
504 |
|
505 |
|
506 |
|
507 | onOpen: PropTypes.func,
|
508 |
|
509 | |
510 |
|
511 |
|
512 | open: PropTypes.bool,
|
513 |
|
514 | |
515 |
|
516 |
|
517 | readOnly: PropTypes.bool,
|
518 |
|
519 | |
520 |
|
521 |
|
522 |
|
523 |
|
524 |
|
525 | renderValue: PropTypes.func,
|
526 |
|
527 | |
528 |
|
529 |
|
530 | SelectDisplayProps: PropTypes.object,
|
531 |
|
532 | |
533 |
|
534 |
|
535 | tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
536 |
|
537 | |
538 |
|
539 |
|
540 | type: PropTypes.any,
|
541 |
|
542 | |
543 |
|
544 |
|
545 | value: PropTypes.any,
|
546 |
|
547 | |
548 |
|
549 |
|
550 | variant: PropTypes.oneOf(['standard', 'outlined', 'filled'])
|
551 | } : void 0;
|
552 | export default SelectInput; |
\ | No newline at end of file |