@carbon/react
Version:
React components for the Carbon Design System
273 lines (264 loc) • 9.95 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
;
Object.defineProperty(exports, '__esModule', { value: true });
var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js');
var PropTypes = require('prop-types');
var React = require('react');
var cx = require('classnames');
var iconsReact = require('@carbon/icons-react');
var deprecate = require('../../prop-types/deprecate.js');
var usePrefix = require('../../internal/usePrefix.js');
require('../FluidForm/FluidForm.js');
var FormContext = require('../FluidForm/FormContext.js');
var events = require('../../tools/events.js');
var Text = require('../Text/Text.js');
require('../Text/TextDirection.js');
var index = require('../AILabel/index.js');
var utils = require('../../internal/utils.js');
var useNormalizedInputProps = require('../../internal/useNormalizedInputProps.js');
const Select = /*#__PURE__*/React.forwardRef(({
className,
decorator,
id,
inline = false,
labelText = 'Select',
disabled = false,
children,
// reserved for use with Pagination component
noLabel = false,
hideLabel = false,
invalid = false,
invalidText = '',
helperText = '',
light = false,
readOnly,
size,
warn = false,
warnText,
onChange,
slug,
...other
}, ref) => {
const prefix = usePrefix.usePrefix();
const {
isFluid
} = React.useContext(FormContext.FormContext);
const [isFocused, setIsFocused] = React.useState(false);
// Convert children to an array of valid elements once using type narrowing
const validChildren = React.Children.toArray(children).filter(child => /*#__PURE__*/React.isValidElement(child));
// Find the default option based on the specified defaultValue or value
const selectedValue = other?.value || other?.defaultValue;
const selectedOption = validChildren.find(child => child.props?.value === selectedValue);
// Use the provided title prop, or the selected option's text if available;
// otherwise, fallback to the first option's text
const initialTitle = other?.title || selectedOption?.props?.text || validChildren[0]?.props?.text || '';
const [title, setTitle] = React.useState(initialTitle);
const normalizedProps = useNormalizedInputProps.useNormalizedInputProps({
id,
disabled,
readOnly,
invalid,
invalidText,
warn,
warnText
});
const selectClasses = cx({
[`${prefix}--select`]: true,
[`${prefix}--select--inline`]: inline,
[`${prefix}--select--light`]: light,
[`${prefix}--select--invalid`]: normalizedProps.invalid,
[`${prefix}--select--disabled`]: normalizedProps.disabled,
[`${prefix}--select--readonly`]: readOnly,
[`${prefix}--select--warning`]: normalizedProps.warn,
[`${prefix}--select--fluid--invalid`]: isFluid && normalizedProps.invalid,
[`${prefix}--select--fluid--focus`]: isFluid && isFocused,
[`${prefix}--select--slug`]: slug,
[`${prefix}--select--decorator`]: decorator
});
const labelClasses = cx(`${prefix}--label`, {
[`${prefix}--visually-hidden`]: hideLabel,
[`${prefix}--label--disabled`]: normalizedProps.disabled
});
const inputClasses = cx({
[`${prefix}--select-input`]: true,
[`${prefix}--select-input--${size}`]: size
});
const error = normalizedProps.validation;
const helperTextClasses = cx(`${prefix}--form__helper-text`, {
[`${prefix}--form__helper-text--disabled`]: normalizedProps.disabled
});
const hasHelper = typeof helperText !== 'undefined' && helperText !== null;
const helper = hasHelper ? /*#__PURE__*/React.createElement(Text.Text, {
as: "div",
id: normalizedProps.helperId,
className: helperTextClasses
}, helperText) : null;
const ariaProps = {};
if (normalizedProps.invalid) {
ariaProps['aria-describedby'] = normalizedProps.invalidId;
} else if (!inline && !isFluid) {
ariaProps['aria-describedby'] = helper ? normalizedProps.helperId : undefined;
}
const handleFocus = evt => {
setIsFocused(evt.type === 'focus');
};
const handleChange = evt => {
const selectedOption = evt?.target?.options[evt.target.selectedIndex];
setTitle(selectedOption?.text);
};
const readOnlyEventHandlers = {
onMouseDown: evt => {
// NOTE: does not prevent click
if (readOnly) {
evt.preventDefault();
// focus on the element as per readonly input behavior
evt.target.focus();
}
},
onKeyDown: evt => {
const selectAccessKeys = ['ArrowDown', 'ArrowUp', ' '];
// This prevents the select from opening for the above keys
if (readOnly && selectAccessKeys.includes(evt.key)) {
evt.preventDefault();
}
}
};
// AILabel always size `mini`
const candidate = slug ?? decorator;
const candidateIsAILabel = utils.isComponentElement(candidate, index.AILabel);
const normalizedDecorator = candidateIsAILabel ? /*#__PURE__*/React.cloneElement(candidate, {
size: 'mini'
}) : candidate;
const input = (() => {
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("select", _rollupPluginBabelHelpers.extends({}, other, ariaProps, {
id: id,
className: inputClasses,
disabled: normalizedProps.disabled || undefined,
"aria-invalid": normalizedProps.invalid || undefined,
"aria-readonly": readOnly || undefined,
title: title,
onChange: events.composeEventHandlers([onChange, handleChange])
}, readOnlyEventHandlers, {
ref: ref
}), children), /*#__PURE__*/React.createElement(iconsReact.ChevronDown, {
className: `${prefix}--select__arrow`
}), normalizedProps.invalid && /*#__PURE__*/React.createElement(iconsReact.WarningFilled, {
className: `${prefix}--select__invalid-icon`
}), !normalizedProps.invalid && normalizedProps.warn && /*#__PURE__*/React.createElement(iconsReact.WarningAltFilled, {
className: `${prefix}--select__invalid-icon ${prefix}--select__invalid-icon--warning`
}));
})();
return /*#__PURE__*/React.createElement("div", {
className: cx(`${prefix}--form-item`, className)
}, /*#__PURE__*/React.createElement("div", {
className: selectClasses
}, !noLabel && /*#__PURE__*/React.createElement(Text.Text, {
as: "label",
htmlFor: id,
className: labelClasses
}, labelText), inline && /*#__PURE__*/React.createElement("div", {
className: `${prefix}--select-input--inline__wrapper`
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--select-input__wrapper`,
"data-invalid": normalizedProps.invalid || null
}, input), error), !inline && /*#__PURE__*/React.createElement("div", {
className: `${prefix}--select-input__wrapper`,
"data-invalid": normalizedProps.invalid || null,
onFocus: handleFocus,
onBlur: handleFocus
}, input, slug ? normalizedDecorator : decorator ? /*#__PURE__*/React.createElement("div", {
className: `${prefix}--select__inner-wrapper--decorator`
}, normalizedDecorator) : '', isFluid && /*#__PURE__*/React.createElement("hr", {
className: `${prefix}--select__divider`
}), isFluid && error ? error : null), !inline && !isFluid && error ? error : helper));
});
Select.displayName = 'Select';
Select.propTypes = {
/**
* Provide the contents of your Select
*/
children: PropTypes.node,
/**
* Specify an optional className to be applied to the node containing the label and the select box
*/
className: PropTypes.string,
/**
* **Experimental**: Provide a decorator component to be rendered inside the `Select` component
*/
decorator: PropTypes.node,
/**
* Optionally provide the default value of the `<select>`
*/
defaultValue: PropTypes.any,
/**
* Specify whether the control is disabled
*/
disabled: PropTypes.bool,
/**
* Provide text that is used alongside the control label for additional help
*/
helperText: PropTypes.node,
/**
* Specify whether the label should be hidden, or not
*/
hideLabel: PropTypes.bool,
/**
* Specify a custom `id` for the `<select>`
*/
id: PropTypes.string.isRequired,
/**
* Specify whether you want the inline version of this control
*/
inline: PropTypes.bool,
/**
* Specify if the currently value is invalid.
*/
invalid: PropTypes.bool,
/**
* Message which is displayed if the value is invalid.
*/
invalidText: PropTypes.node,
/**
* Provide label text to be read by screen readers when interacting with the
* control
*/
labelText: PropTypes.node,
/**
* `true` to use the light version. For use on $ui-01 backgrounds only.
* Don't use this to make tile background color same as container background color.
*/
light: deprecate.deprecate(PropTypes.bool, 'The `light` prop for `Select` is no longer needed and has ' + 'been deprecated in v11 in favor of the new `Layer` component. It will be moved in the next major release.'),
/**
* Reserved for use with Pagination component. Will not render a label for the
* select since Pagination renders one for us.
*/
noLabel: PropTypes.bool,
/**
* Provide an optional `onChange` hook that is called each time the value of
* the underlying `<input>` changes
*/
onChange: PropTypes.func,
/**
* Whether the select should be read-only
*/
readOnly: PropTypes.bool,
/**
* Specify the size of the Select Input.
*/
size: PropTypes.oneOf(['sm', 'md', 'lg']),
slug: deprecate.deprecate(PropTypes.node, 'The `slug` prop has been deprecated and will be removed in the next major version. Use the decorator prop instead.'),
/**
* Specify whether the control is currently in warning state
*/
warn: PropTypes.bool,
/**
* Provide the text that is displayed when the control is in warning state
*/
warnText: PropTypes.node
};
exports.default = Select;