UNPKG

17.7 kBJavaScriptView Raw
1"use strict";
2
3var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
4
5var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
6
7Object.defineProperty(exports, "__esModule", {
8 value: true
9});
10exports.default = void 0;
11
12var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
13
14var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
15
16var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
17
18var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
19
20var _utils = require("@material-ui/utils");
21
22var React = _interopRequireWildcard(require("react"));
23
24var _reactIs = require("react-is");
25
26var _propTypes = _interopRequireDefault(require("prop-types"));
27
28var _clsx = _interopRequireDefault(require("clsx"));
29
30var _ownerDocument = _interopRequireDefault(require("../utils/ownerDocument"));
31
32var _capitalize = _interopRequireDefault(require("../utils/capitalize"));
33
34var _Menu = _interopRequireDefault(require("../Menu/Menu"));
35
36var _utils2 = require("../InputBase/utils");
37
38var _useForkRef = _interopRequireDefault(require("../utils/useForkRef"));
39
40var _useControlled3 = _interopRequireDefault(require("../utils/useControlled"));
41
42function areEqualValues(a, b) {
43 if ((0, _typeof2.default)(b) === 'object' && b !== null) {
44 return a === b;
45 }
46
47 return String(a) === String(b);
48}
49
50function isEmpty(display) {
51 return display == null || typeof display === 'string' && !display.trim();
52}
53/**
54 * @ignore - internal component.
55 */
56
57
58var SelectInput = /*#__PURE__*/React.forwardRef(function SelectInput(props, ref) {
59 var ariaLabel = props['aria-label'],
60 autoFocus = props.autoFocus,
61 autoWidth = props.autoWidth,
62 children = props.children,
63 classes = props.classes,
64 className = props.className,
65 defaultValue = props.defaultValue,
66 disabled = props.disabled,
67 displayEmpty = props.displayEmpty,
68 IconComponent = props.IconComponent,
69 inputRefProp = props.inputRef,
70 labelId = props.labelId,
71 _props$MenuProps = props.MenuProps,
72 MenuProps = _props$MenuProps === void 0 ? {} : _props$MenuProps,
73 multiple = props.multiple,
74 name = props.name,
75 onBlur = props.onBlur,
76 onChange = props.onChange,
77 onClose = props.onClose,
78 onFocus = props.onFocus,
79 onOpen = props.onOpen,
80 openProp = props.open,
81 readOnly = props.readOnly,
82 renderValue = props.renderValue,
83 _props$SelectDisplayP = props.SelectDisplayProps,
84 SelectDisplayProps = _props$SelectDisplayP === void 0 ? {} : _props$SelectDisplayP,
85 tabIndexProp = props.tabIndex,
86 type = props.type,
87 valueProp = props.value,
88 _props$variant = props.variant,
89 variant = _props$variant === void 0 ? 'standard' : _props$variant,
90 other = (0, _objectWithoutProperties2.default)(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"]);
91
92 var _useControlled = (0, _useControlled3.default)({
93 controlled: valueProp,
94 default: defaultValue,
95 name: 'Select'
96 }),
97 _useControlled2 = (0, _slicedToArray2.default)(_useControlled, 2),
98 value = _useControlled2[0],
99 setValue = _useControlled2[1];
100
101 var inputRef = React.useRef(null);
102
103 var _React$useState = React.useState(null),
104 displayNode = _React$useState[0],
105 setDisplayNode = _React$useState[1];
106
107 var _React$useRef = React.useRef(openProp != null),
108 isOpenControlled = _React$useRef.current;
109
110 var _React$useState2 = React.useState(),
111 menuMinWidthState = _React$useState2[0],
112 setMenuMinWidthState = _React$useState2[1];
113
114 var _React$useState3 = React.useState(false),
115 openState = _React$useState3[0],
116 setOpenState = _React$useState3[1];
117
118 var handleRef = (0, _useForkRef.default)(ref, inputRefProp);
119 React.useImperativeHandle(handleRef, function () {
120 return {
121 focus: function focus() {
122 displayNode.focus();
123 },
124 node: inputRef.current,
125 value: value
126 };
127 }, [displayNode, value]);
128 React.useEffect(function () {
129 if (autoFocus && displayNode) {
130 displayNode.focus();
131 }
132 }, [autoFocus, displayNode]);
133 React.useEffect(function () {
134 if (displayNode) {
135 var label = (0, _ownerDocument.default)(displayNode).getElementById(labelId);
136
137 if (label) {
138 var handler = function handler() {
139 if (getSelection().isCollapsed) {
140 displayNode.focus();
141 }
142 };
143
144 label.addEventListener('click', handler);
145 return function () {
146 label.removeEventListener('click', handler);
147 };
148 }
149 }
150
151 return undefined;
152 }, [labelId, displayNode]);
153
154 var update = function update(open, event) {
155 if (open) {
156 if (onOpen) {
157 onOpen(event);
158 }
159 } else if (onClose) {
160 onClose(event);
161 }
162
163 if (!isOpenControlled) {
164 setMenuMinWidthState(autoWidth ? null : displayNode.clientWidth);
165 setOpenState(open);
166 }
167 };
168
169 var handleMouseDown = function handleMouseDown(event) {
170 // Ignore everything but left-click
171 if (event.button !== 0) {
172 return;
173 } // Hijack the default focus behavior.
174
175
176 event.preventDefault();
177 displayNode.focus();
178 update(true, event);
179 };
180
181 var handleClose = function handleClose(event) {
182 update(false, event);
183 };
184
185 var childrenArray = React.Children.toArray(children); // Support autofill.
186
187 var handleChange = function handleChange(event) {
188 var index = childrenArray.map(function (child) {
189 return child.props.value;
190 }).indexOf(event.target.value);
191
192 if (index === -1) {
193 return;
194 }
195
196 var child = childrenArray[index];
197 setValue(child.props.value);
198
199 if (onChange) {
200 onChange(event, child);
201 }
202 };
203
204 var handleItemClick = function handleItemClick(child) {
205 return function (event) {
206 if (!multiple) {
207 update(false, event);
208 }
209
210 var newValue;
211
212 if (multiple) {
213 newValue = Array.isArray(value) ? value.slice() : [];
214 var itemIndex = value.indexOf(child.props.value);
215
216 if (itemIndex === -1) {
217 newValue.push(child.props.value);
218 } else {
219 newValue.splice(itemIndex, 1);
220 }
221 } else {
222 newValue = child.props.value;
223 }
224
225 if (child.props.onClick) {
226 child.props.onClick(event);
227 }
228
229 if (value === newValue) {
230 return;
231 }
232
233 setValue(newValue);
234
235 if (onChange) {
236 event.persist(); // Preact support, target is read only property on a native event.
237
238 Object.defineProperty(event, 'target', {
239 writable: true,
240 value: {
241 value: newValue,
242 name: name
243 }
244 });
245 onChange(event, child);
246 }
247 };
248 };
249
250 var handleKeyDown = function handleKeyDown(event) {
251 if (!readOnly) {
252 var validKeys = [' ', 'ArrowUp', 'ArrowDown', // The native select doesn't respond to enter on MacOS, but it's recommended by
253 // https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-collapsible.html
254 'Enter'];
255
256 if (validKeys.indexOf(event.key) !== -1) {
257 event.preventDefault();
258 update(true, event);
259 }
260 }
261 };
262
263 var open = displayNode !== null && (isOpenControlled ? openProp : openState);
264
265 var handleBlur = function handleBlur(event) {
266 // if open event.stopImmediatePropagation
267 if (!open && onBlur) {
268 event.persist(); // Preact support, target is read only property on a native event.
269
270 Object.defineProperty(event, 'target', {
271 writable: true,
272 value: {
273 value: value,
274 name: name
275 }
276 });
277 onBlur(event);
278 }
279 };
280
281 delete other['aria-invalid'];
282 var display;
283 var displaySingle;
284 var displayMultiple = [];
285 var computeDisplay = false;
286 var foundMatch = false; // No need to display any value if the field is empty.
287
288 if ((0, _utils2.isFilled)({
289 value: value
290 }) || displayEmpty) {
291 if (renderValue) {
292 display = renderValue(value);
293 } else {
294 computeDisplay = true;
295 }
296 }
297
298 var items = childrenArray.map(function (child) {
299 if (! /*#__PURE__*/React.isValidElement(child)) {
300 return null;
301 }
302
303 if (process.env.NODE_ENV !== 'production') {
304 if ((0, _reactIs.isFragment)(child)) {
305 console.error(["Material-UI: The Select component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n'));
306 }
307 }
308
309 var selected;
310
311 if (multiple) {
312 if (!Array.isArray(value)) {
313 throw new Error(process.env.NODE_ENV !== "production" ? "Material-UI: The `value` prop must be an array when using the `Select` component with `multiple`." : (0, _utils.formatMuiErrorMessage)(2));
314 }
315
316 selected = value.some(function (v) {
317 return areEqualValues(v, child.props.value);
318 });
319
320 if (selected && computeDisplay) {
321 displayMultiple.push(child.props.children);
322 }
323 } else {
324 selected = areEqualValues(value, child.props.value);
325
326 if (selected && computeDisplay) {
327 displaySingle = child.props.children;
328 }
329 }
330
331 if (selected) {
332 foundMatch = true;
333 }
334
335 return /*#__PURE__*/React.cloneElement(child, {
336 'aria-selected': selected ? 'true' : undefined,
337 onClick: handleItemClick(child),
338 onKeyUp: function onKeyUp(event) {
339 if (event.key === ' ') {
340 // otherwise our MenuItems dispatches a click event
341 // it's not behavior of the native <option> and causes
342 // the select to close immediately since we open on space keydown
343 event.preventDefault();
344 }
345
346 if (child.props.onKeyUp) {
347 child.props.onKeyUp(event);
348 }
349 },
350 role: 'option',
351 selected: selected,
352 value: undefined,
353 // The value is most likely not a valid HTML attribute.
354 'data-value': child.props.value // Instead, we provide it as a data attribute.
355
356 });
357 });
358
359 if (process.env.NODE_ENV !== 'production') {
360 // eslint-disable-next-line react-hooks/rules-of-hooks
361 React.useEffect(function () {
362 if (!foundMatch && !multiple && value !== '') {
363 var values = childrenArray.map(function (child) {
364 return child.props.value;
365 });
366 console.warn(["Material-UI: You have provided an out-of-range value `".concat(value, "` for the select ").concat(name ? "(name=\"".concat(name, "\") ") : '', "component."), "Consider providing a value that matches one of the available options or ''.", "The available values are ".concat(values.filter(function (x) {
367 return x != null;
368 }).map(function (x) {
369 return "`".concat(x, "`");
370 }).join(', ') || '""', ".")].join('\n'));
371 }
372 }, [foundMatch, childrenArray, multiple, name, value]);
373 }
374
375 if (computeDisplay) {
376 display = multiple ? displayMultiple.join(', ') : displaySingle;
377 } // Avoid performing a layout computation in the render method.
378
379
380 var menuMinWidth = menuMinWidthState;
381
382 if (!autoWidth && isOpenControlled && displayNode) {
383 menuMinWidth = displayNode.clientWidth;
384 }
385
386 var tabIndex;
387
388 if (typeof tabIndexProp !== 'undefined') {
389 tabIndex = tabIndexProp;
390 } else {
391 tabIndex = disabled ? null : 0;
392 }
393
394 var buttonId = SelectDisplayProps.id || (name ? "mui-component-select-".concat(name) : undefined);
395 return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", (0, _extends2.default)({
396 className: (0, _clsx.default)(classes.root, // TODO v5: merge root and select
397 classes.select, classes.selectMenu, classes[variant], className, disabled && classes.disabled),
398 ref: setDisplayNode,
399 tabIndex: tabIndex,
400 role: "button",
401 "aria-disabled": disabled ? 'true' : undefined,
402 "aria-expanded": open ? 'true' : undefined,
403 "aria-haspopup": "listbox",
404 "aria-label": ariaLabel,
405 "aria-labelledby": [labelId, buttonId].filter(Boolean).join(' ') || undefined,
406 onKeyDown: handleKeyDown,
407 onMouseDown: disabled || readOnly ? null : handleMouseDown,
408 onBlur: handleBlur,
409 onFocus: onFocus
410 }, SelectDisplayProps, {
411 // The id is required for proper a11y
412 id: buttonId
413 }), isEmpty(display) ?
414 /*#__PURE__*/
415 // eslint-disable-next-line react/no-danger
416 React.createElement("span", {
417 dangerouslySetInnerHTML: {
418 __html: '&#8203;'
419 }
420 }) : display), /*#__PURE__*/React.createElement("input", (0, _extends2.default)({
421 value: Array.isArray(value) ? value.join(',') : value,
422 name: name,
423 ref: inputRef,
424 "aria-hidden": true,
425 onChange: handleChange,
426 tabIndex: -1,
427 className: classes.nativeInput,
428 autoFocus: autoFocus
429 }, other)), /*#__PURE__*/React.createElement(IconComponent, {
430 className: (0, _clsx.default)(classes.icon, classes["icon".concat((0, _capitalize.default)(variant))], open && classes.iconOpen, disabled && classes.disabled)
431 }), /*#__PURE__*/React.createElement(_Menu.default, (0, _extends2.default)({
432 id: "menu-".concat(name || ''),
433 anchorEl: displayNode,
434 open: open,
435 onClose: handleClose
436 }, MenuProps, {
437 MenuListProps: (0, _extends2.default)({
438 'aria-labelledby': labelId,
439 role: 'listbox',
440 disableListWrap: true
441 }, MenuProps.MenuListProps),
442 PaperProps: (0, _extends2.default)({}, MenuProps.PaperProps, {
443 style: (0, _extends2.default)({
444 minWidth: menuMinWidth
445 }, MenuProps.PaperProps != null ? MenuProps.PaperProps.style : null)
446 })
447 }), items));
448});
449process.env.NODE_ENV !== "production" ? SelectInput.propTypes = {
450 /**
451 * @ignore
452 */
453 'aria-label': _propTypes.default.string,
454
455 /**
456 * @ignore
457 */
458 autoFocus: _propTypes.default.bool,
459
460 /**
461 * If `true`, the width of the popover will automatically be set according to the items inside the
462 * menu, otherwise it will be at least the width of the select input.
463 */
464 autoWidth: _propTypes.default.bool,
465
466 /**
467 * The option elements to populate the select with.
468 * Can be some `<MenuItem>` elements.
469 */
470 children: _propTypes.default.node,
471
472 /**
473 * Override or extend the styles applied to the component.
474 * See [CSS API](#css) below for more details.
475 */
476 classes: _propTypes.default.object.isRequired,
477
478 /**
479 * The CSS class name of the select element.
480 */
481 className: _propTypes.default.string,
482
483 /**
484 * The default element value. Use when the component is not controlled.
485 */
486 defaultValue: _propTypes.default.any,
487
488 /**
489 * If `true`, the select will be disabled.
490 */
491 disabled: _propTypes.default.bool,
492
493 /**
494 * If `true`, the selected item is displayed even if its value is empty.
495 */
496 displayEmpty: _propTypes.default.bool,
497
498 /**
499 * The icon that displays the arrow.
500 */
501 IconComponent: _propTypes.default.elementType.isRequired,
502
503 /**
504 * Imperative handle implementing `{ value: T, node: HTMLElement, focus(): void }`
505 * Equivalent to `ref`
506 */
507 inputRef: _utils.refType,
508
509 /**
510 * The ID of an element that acts as an additional label. The Select will
511 * be labelled by the additional label and the selected value.
512 */
513 labelId: _propTypes.default.string,
514
515 /**
516 * Props applied to the [`Menu`](/api/menu/) element.
517 */
518 MenuProps: _propTypes.default.object,
519
520 /**
521 * If `true`, `value` must be an array and the menu will support multiple selections.
522 */
523 multiple: _propTypes.default.bool,
524
525 /**
526 * Name attribute of the `select` or hidden `input` element.
527 */
528 name: _propTypes.default.string,
529
530 /**
531 * @ignore
532 */
533 onBlur: _propTypes.default.func,
534
535 /**
536 * Callback function fired when a menu item is selected.
537 *
538 * @param {object} event The event source of the callback.
539 * You can pull out the new value by accessing `event.target.value` (any).
540 * @param {object} [child] The react element that was selected.
541 */
542 onChange: _propTypes.default.func,
543
544 /**
545 * Callback fired when the component requests to be closed.
546 * Use in controlled mode (see open).
547 *
548 * @param {object} event The event source of the callback.
549 */
550 onClose: _propTypes.default.func,
551
552 /**
553 * @ignore
554 */
555 onFocus: _propTypes.default.func,
556
557 /**
558 * Callback fired when the component requests to be opened.
559 * Use in controlled mode (see open).
560 *
561 * @param {object} event The event source of the callback.
562 */
563 onOpen: _propTypes.default.func,
564
565 /**
566 * Control `select` open state.
567 */
568 open: _propTypes.default.bool,
569
570 /**
571 * @ignore
572 */
573 readOnly: _propTypes.default.bool,
574
575 /**
576 * Render the selected value.
577 *
578 * @param {any} value The `value` provided to the component.
579 * @returns {ReactNode}
580 */
581 renderValue: _propTypes.default.func,
582
583 /**
584 * Props applied to the clickable div element.
585 */
586 SelectDisplayProps: _propTypes.default.object,
587
588 /**
589 * @ignore
590 */
591 tabIndex: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.string]),
592
593 /**
594 * @ignore
595 */
596 type: _propTypes.default.any,
597
598 /**
599 * The input value.
600 */
601 value: _propTypes.default.any,
602
603 /**
604 * The variant to use.
605 */
606 variant: _propTypes.default.oneOf(['standard', 'outlined', 'filled'])
607} : void 0;
608var _default = SelectInput;
609exports.default = _default;
\No newline at end of file