UNPKG

@mui/material

Version:

Quickly build beautiful React apps. MUI is a simple and customizable component library to build faster, beautiful, and more accessible React applications. Follow your own design system, or start with Material Design.

687 lines (599 loc) 18.8 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose"; import { formatMuiErrorMessage as _formatMuiErrorMessage } from "@mui/utils"; const _excluded = ["aria-describedby", "aria-label", "autoFocus", "autoWidth", "children", "className", "defaultValue", "disabled", "displayEmpty", "IconComponent", "inputRef", "labelId", "MenuProps", "multiple", "name", "onBlur", "onChange", "onClose", "onFocus", "onOpen", "open", "readOnly", "renderValue", "SelectDisplayProps", "tabIndex", "type", "value", "variant"]; import * as React from 'react'; import { isFragment } from 'react-is'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { unstable_composeClasses as composeClasses } from '@mui/base'; import { refType } from '@mui/utils'; import ownerDocument from '../utils/ownerDocument'; import capitalize from '../utils/capitalize'; import Menu from '../Menu/Menu'; import { nativeSelectSelectStyles, nativeSelectIconStyles } from '../NativeSelect/NativeSelectInput'; import { isFilled } from '../InputBase/utils'; import styled, { slotShouldForwardProp } from '../styles/styled'; import useForkRef from '../utils/useForkRef'; import useControlled from '../utils/useControlled'; import selectClasses, { getSelectUtilityClasses } from './selectClasses'; import { jsx as _jsx } from "react/jsx-runtime"; import { jsxs as _jsxs } from "react/jsx-runtime"; const SelectSelect = styled('div', { name: 'MuiSelect', slot: 'Select', overridesResolver: (props, styles) => { const { ownerState } = props; return [// Win specificity over the input base { [`&.${selectClasses.select}`]: styles.select }, { [`&.${selectClasses.select}`]: styles[ownerState.variant] }, { [`&.${selectClasses.multiple}`]: styles.multiple }]; } })(nativeSelectSelectStyles, { // Win specificity over the input base [`&.${selectClasses.select}`]: { height: 'auto', // Resets for multiple select with chips minHeight: '1.4375em', // Required for select\text-field height consistency textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' } }); const SelectIcon = styled('svg', { name: 'MuiSelect', slot: 'Icon', overridesResolver: (props, styles) => { const { ownerState } = props; return [styles.icon, ownerState.variant && styles[`icon${capitalize(ownerState.variant)}`], ownerState.open && styles.iconOpen]; } })(nativeSelectIconStyles); const SelectNativeInput = styled('input', { shouldForwardProp: prop => slotShouldForwardProp(prop) && prop !== 'classes', name: 'MuiSelect', slot: 'NativeInput', overridesResolver: (props, styles) => styles.nativeInput })({ bottom: 0, left: 0, position: 'absolute', opacity: 0, pointerEvents: 'none', width: '100%', boxSizing: 'border-box' }); function areEqualValues(a, b) { if (typeof b === 'object' && b !== null) { return a === b; } // The value could be a number, the DOM will stringify it anyway. return String(a) === String(b); } function isEmpty(display) { return display == null || typeof display === 'string' && !display.trim(); } const useUtilityClasses = ownerState => { const { classes, variant, disabled, multiple, open } = ownerState; const slots = { select: ['select', variant, disabled && 'disabled', multiple && 'multiple'], icon: ['icon', `icon${capitalize(variant)}`, open && 'iconOpen', disabled && 'disabled'], nativeInput: ['nativeInput'] }; return composeClasses(slots, getSelectUtilityClasses, classes); }; /** * @ignore - internal component. */ const SelectInput = /*#__PURE__*/React.forwardRef(function SelectInput(props, ref) { const { 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, autoFocus, autoWidth, children, className, defaultValue, disabled, displayEmpty, IconComponent, inputRef: inputRefProp, labelId, MenuProps = {}, multiple, name, onBlur, onChange, onClose, onFocus, onOpen, open: openProp, readOnly, renderValue, SelectDisplayProps = {}, tabIndex: tabIndexProp, value: valueProp, variant = 'standard' } = props, other = _objectWithoutPropertiesLoose(props, _excluded); const [value, setValueState] = useControlled({ controlled: valueProp, default: defaultValue, name: 'Select' }); const inputRef = React.useRef(null); const displayRef = React.useRef(null); const [displayNode, setDisplayNode] = React.useState(null); const { current: isOpenControlled } = React.useRef(openProp != null); const [menuMinWidthState, setMenuMinWidthState] = React.useState(); const [openState, setOpenState] = React.useState(false); const handleRef = useForkRef(ref, inputRefProp); const handleDisplayRef = React.useCallback(node => { displayRef.current = node; if (node) { setDisplayNode(node); } }, []); React.useImperativeHandle(handleRef, () => ({ focus: () => { displayRef.current.focus(); }, node: inputRef.current, value }), [value]); React.useEffect(() => { if (autoFocus) { displayRef.current.focus(); } }, [autoFocus]); React.useEffect(() => { const label = ownerDocument(displayRef.current).getElementById(labelId); if (label) { const handler = () => { if (getSelection().isCollapsed) { displayRef.current.focus(); } }; label.addEventListener('click', handler); return () => { label.removeEventListener('click', handler); }; } return undefined; }, [labelId]); const update = (open, event) => { if (open) { if (onOpen) { onOpen(event); } } else if (onClose) { onClose(event); } if (!isOpenControlled) { setMenuMinWidthState(autoWidth ? null : displayNode.clientWidth); setOpenState(open); } }; const handleMouseDown = event => { // Ignore everything but left-click if (event.button !== 0) { return; } // Hijack the default focus behavior. event.preventDefault(); displayRef.current.focus(); update(true, event); }; const handleClose = event => { update(false, event); }; const childrenArray = React.Children.toArray(children); // Support autofill. const handleChange = event => { const index = childrenArray.map(child => child.props.value).indexOf(event.target.value); if (index === -1) { return; } const child = childrenArray[index]; setValueState(child.props.value); if (onChange) { onChange(event, child); } }; const handleItemClick = child => event => { let newValue; // We use the tabindex attribute to signal the available options. if (!event.currentTarget.hasAttribute('tabindex')) { return; } if (multiple) { newValue = Array.isArray(value) ? value.slice() : []; const itemIndex = value.indexOf(child.props.value); if (itemIndex === -1) { newValue.push(child.props.value); } else { newValue.splice(itemIndex, 1); } } else { newValue = child.props.value; } if (child.props.onClick) { child.props.onClick(event); } if (value !== newValue) { setValueState(newValue); if (onChange) { // Redefine target to allow name and value to be read. // This allows seamless integration with the most popular form libraries. // https://github.com/mui-org/material-ui/issues/13485#issuecomment-676048492 // Clone the event to not override `target` of the original event. const nativeEvent = event.nativeEvent || event; const clonedEvent = new nativeEvent.constructor(nativeEvent.type, nativeEvent); Object.defineProperty(clonedEvent, 'target', { writable: true, value: { value: newValue, name } }); onChange(clonedEvent, child); } } if (!multiple) { update(false, event); } }; const handleKeyDown = event => { if (!readOnly) { const validKeys = [' ', 'ArrowUp', 'ArrowDown', // The native select doesn't respond to enter on MacOS, but it's recommended by // https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-collapsible.html 'Enter']; if (validKeys.indexOf(event.key) !== -1) { event.preventDefault(); update(true, event); } } }; const open = displayNode !== null && (isOpenControlled ? openProp : openState); const handleBlur = event => { // if open event.stopImmediatePropagation if (!open && onBlur) { // Preact support, target is read only property on a native event. Object.defineProperty(event, 'target', { writable: true, value: { value, name } }); onBlur(event); } }; delete other['aria-invalid']; let display; let displaySingle; const displayMultiple = []; let computeDisplay = false; let foundMatch = false; // No need to display any value if the field is empty. if (isFilled({ value }) || displayEmpty) { if (renderValue) { display = renderValue(value); } else { computeDisplay = true; } } const items = childrenArray.map(child => { if (! /*#__PURE__*/React.isValidElement(child)) { return null; } if (process.env.NODE_ENV !== 'production') { if (isFragment(child)) { console.error(["MUI: The Select component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n')); } } let selected; if (multiple) { if (!Array.isArray(value)) { throw new Error(process.env.NODE_ENV !== "production" ? `MUI: The \`value\` prop must be an array when using the \`Select\` component with \`multiple\`.` : _formatMuiErrorMessage(2)); } selected = value.some(v => areEqualValues(v, child.props.value)); if (selected && computeDisplay) { displayMultiple.push(child.props.children); } } else { selected = areEqualValues(value, child.props.value); if (selected && computeDisplay) { displaySingle = child.props.children; } } if (selected) { foundMatch = true; } return /*#__PURE__*/React.cloneElement(child, { 'aria-selected': selected ? 'true' : 'false', onClick: handleItemClick(child), onKeyUp: event => { if (event.key === ' ') { // otherwise our MenuItems dispatches a click event // it's not behavior of the native <option> and causes // the select to close immediately since we open on space keydown event.preventDefault(); } if (child.props.onKeyUp) { child.props.onKeyUp(event); } }, role: 'option', selected, value: undefined, // The value is most likely not a valid HTML attribute. 'data-value': child.props.value // Instead, we provide it as a data attribute. }); }); if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line react-hooks/rules-of-hooks React.useEffect(() => { if (!foundMatch && !multiple && value !== '') { const values = childrenArray.map(child => child.props.value); console.warn([`MUI: 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')); } }, [foundMatch, childrenArray, multiple, name, value]); } if (computeDisplay) { if (multiple) { if (displayMultiple.length === 0) { display = null; } else { display = displayMultiple.reduce((output, child, index) => { output.push(child); if (index < displayMultiple.length - 1) { output.push(', '); } return output; }, []); } } else { display = displaySingle; } } // Avoid performing a layout computation in the render method. let menuMinWidth = menuMinWidthState; if (!autoWidth && isOpenControlled && displayNode) { menuMinWidth = displayNode.clientWidth; } let tabIndex; if (typeof tabIndexProp !== 'undefined') { tabIndex = tabIndexProp; } else { tabIndex = disabled ? null : 0; } const buttonId = SelectDisplayProps.id || (name ? `mui-component-select-${name}` : undefined); const ownerState = _extends({}, props, { variant, value, open }); const classes = useUtilityClasses(ownerState); return /*#__PURE__*/_jsxs(React.Fragment, { children: [/*#__PURE__*/_jsx(SelectSelect, _extends({ ref: handleDisplayRef, tabIndex: tabIndex, role: "button", "aria-disabled": disabled ? 'true' : undefined, "aria-expanded": open ? 'true' : 'false', "aria-haspopup": "listbox", "aria-label": ariaLabel, "aria-labelledby": [labelId, buttonId].filter(Boolean).join(' ') || undefined, "aria-describedby": ariaDescribedby, onKeyDown: handleKeyDown, onMouseDown: disabled || readOnly ? null : handleMouseDown, onBlur: handleBlur, onFocus: onFocus }, SelectDisplayProps, { ownerState: ownerState, className: clsx(classes.select, className, SelectDisplayProps.className) // The id is required for proper a11y , id: buttonId, children: isEmpty(display) ? /*#__PURE__*/ // notranslate needed while Google Translate will not fix zero-width space issue // eslint-disable-next-line react/no-danger _jsx("span", { className: "notranslate", dangerouslySetInnerHTML: { __html: '&#8203;' } }) : display })), /*#__PURE__*/_jsx(SelectNativeInput, _extends({ value: Array.isArray(value) ? value.join(',') : value, name: name, ref: inputRef, "aria-hidden": true, onChange: handleChange, tabIndex: -1, disabled: disabled, className: classes.nativeInput, autoFocus: autoFocus, ownerState: ownerState }, other)), /*#__PURE__*/_jsx(SelectIcon, { as: IconComponent, className: classes.icon, ownerState: ownerState }), /*#__PURE__*/_jsx(Menu, _extends({ id: `menu-${name || ''}`, anchorEl: displayNode, open: open, onClose: handleClose, anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, transformOrigin: { vertical: 'top', horizontal: 'center' } }, MenuProps, { MenuListProps: _extends({ 'aria-labelledby': labelId, role: 'listbox', disableListWrap: true }, MenuProps.MenuListProps), PaperProps: _extends({}, MenuProps.PaperProps, { style: _extends({ minWidth: menuMinWidth }, MenuProps.PaperProps != null ? MenuProps.PaperProps.style : null) }), children: items }))] }); }); process.env.NODE_ENV !== "production" ? SelectInput.propTypes = { /** * @ignore */ 'aria-describedby': PropTypes.string, /** * @ignore */ 'aria-label': PropTypes.string, /** * @ignore */ autoFocus: PropTypes.bool, /** * If `true`, the width of the popover will automatically be set according to the items inside the * menu, otherwise it will be at least the width of the select input. */ autoWidth: PropTypes.bool, /** * The option elements to populate the select with. * Can be some `<MenuItem>` elements. */ children: PropTypes.node, /** * Override or extend the styles applied to the component. * See [CSS API](#css) below for more details. */ classes: PropTypes.object, /** * The CSS class name of the select element. */ className: PropTypes.string, /** * The default value. Use when the component is not controlled. */ defaultValue: PropTypes.any, /** * If `true`, the select is disabled. */ disabled: PropTypes.bool, /** * If `true`, the selected item is displayed even if its value is empty. */ displayEmpty: PropTypes.bool, /** * The icon that displays the arrow. */ IconComponent: PropTypes.elementType.isRequired, /** * Imperative handle implementing `{ value: T, node: HTMLElement, focus(): void }` * Equivalent to `ref` */ inputRef: refType, /** * The ID of an element that acts as an additional label. The Select will * be labelled by the additional label and the selected value. */ labelId: PropTypes.string, /** * Props applied to the [`Menu`](/api/menu/) element. */ MenuProps: PropTypes.object, /** * If `true`, `value` must be an array and the menu will support multiple selections. */ multiple: PropTypes.bool, /** * Name attribute of the `select` or hidden `input` element. */ name: PropTypes.string, /** * @ignore */ onBlur: PropTypes.func, /** * Callback fired when a menu item is selected. * * @param {object} event The event source of the callback. * You can pull out the new value by accessing `event.target.value` (any). * @param {object} [child] The react element that was selected. */ onChange: PropTypes.func, /** * Callback fired when the component requests to be closed. * Use in controlled mode (see open). * * @param {object} event The event source of the callback. */ onClose: PropTypes.func, /** * @ignore */ onFocus: PropTypes.func, /** * Callback fired when the component requests to be opened. * Use in controlled mode (see open). * * @param {object} event The event source of the callback. */ onOpen: PropTypes.func, /** * If `true`, the component is shown. */ open: PropTypes.bool, /** * @ignore */ readOnly: PropTypes.bool, /** * Render the selected value. * * @param {any} value The `value` provided to the component. * @returns {ReactNode} */ renderValue: PropTypes.func, /** * Props applied to the clickable div element. */ SelectDisplayProps: PropTypes.object, /** * @ignore */ tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), /** * @ignore */ type: PropTypes.any, /** * The input value. */ value: PropTypes.any, /** * The variant to use. */ variant: PropTypes.oneOf(['standard', 'outlined', 'filled']) } : void 0; export default SelectInput;