@react-ui-org/react-ui
Version:
React UI is a themeable UI library for React apps.
265 lines (257 loc) • 8.36 kB
JSX
import PropTypes from 'prop-types';
import React, { useContext } from 'react';
import { withGlobalProps } from '../../providers/globalProps';
import { classNames } from '../../helpers/classNames/classNames';
import { transferProps } from '../../helpers/transferProps';
import { getRootSizeClassName } from '../_helpers/getRootSizeClassName';
import { getRootValidationStateClassName } from '../_helpers/getRootValidationStateClassName';
import { resolveContextOrProp } from '../_helpers/resolveContextOrProp';
import { FormLayoutContext } from '../FormLayout';
import { InputGroupContext } from '../InputGroup/InputGroupContext';
import { Option } from './_components/Option';
import styles from './SelectField.module.scss';
export const SelectField = React.forwardRef((props, ref) => {
const {
disabled,
fullWidth,
helpText,
id,
isLabelVisible,
label,
layout,
options,
renderAsRequired,
required,
size,
validationState,
validationText,
variant,
...restProps
} = props;
const formLayoutContext = useContext(FormLayoutContext);
const inputGroupContext = useContext(InputGroupContext);
return (
<label
className={classNames(
styles.root,
fullWidth && styles.isRootFullWidth,
formLayoutContext && styles.isRootInFormLayout,
resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled) && styles.isRootDisabled,
resolveContextOrProp(formLayoutContext && formLayoutContext.layout, layout) === 'horizontal'
? styles.isRootLayoutHorizontal
: styles.isRootLayoutVertical,
inputGroupContext && styles.isRootGrouped,
(renderAsRequired || required) && styles.isRootRequired,
getRootSizeClassName(
resolveContextOrProp(inputGroupContext && inputGroupContext.size, size),
styles,
),
getRootValidationStateClassName(validationState, styles),
variant === 'filled' ? styles.isRootVariantFilled : styles.isRootVariantOutline,
)}
htmlFor={id}
id={id && `${id}__label`}
>
<div
className={classNames(
styles.label,
(!isLabelVisible || inputGroupContext) && styles.isLabelHidden,
)}
id={id && `${id}__labelText`}
>
{label}
</div>
<div className={styles.field}>
<div className={styles.inputContainer}>
<select
{...transferProps(restProps)}
className={styles.input}
disabled={resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled)}
id={id}
ref={ref}
required={required}
>
{
options.map((option) => {
if ('options' in option) {
return (
<optgroup
key={option.key ?? option.label}
label={option.label}
>
{option.options.map((optgroupOption) => (
<Option
key={optgroupOption.key ?? optgroupOption.value}
{...optgroupOption}
{...(id && { id: `${id}__item__${optgroupOption.key ?? optgroupOption.value}` })}
/>
))}
</optgroup>
);
}
return (
<Option
key={option.key ?? option.value}
{...option}
{...(id && { id: `${id}__item__${option.key ?? option.value}` })}
/>
);
})
}
</select>
<div className={styles.caret}>
<span className={styles.caretIcon} />
</div>
{variant === 'filled' && (
<div className={styles.bottomLine} />
)}
</div>
{helpText && (
<div
className={styles.helpText}
id={id && `${id}__helpText`}
>
{helpText}
</div>
)}
{(validationText && !inputGroupContext) && (
<div
className={styles.validationText}
id={id && `${id}__validationText`}
>
{validationText}
</div>
)}
</div>
</label>
);
});
SelectField.defaultProps = {
disabled: false,
fullWidth: false,
helpText: null,
id: undefined,
isLabelVisible: true,
layout: 'vertical',
renderAsRequired: false,
required: false,
size: 'medium',
validationState: null,
validationText: null,
variant: 'outline',
};
SelectField.propTypes = {
/**
* If `true`, the input will be disabled.
*/
disabled: PropTypes.bool,
/**
* If `true`, the field will span the full width of its parent.
*/
fullWidth: PropTypes.bool,
/**
* Optional help text.
*/
helpText: PropTypes.node,
/**
* ID of the input HTML element.
*
* Also serves as a prefix for important inner elements:
* * `<ID>__label`
* * `<ID>__labelText`,
* * `<ID>__helpText`
* * `<ID>__validationText`
*
* and of individual options:
* * `<ID>__item__<VALUE>`
*
* If `key` in the option definition object is set,
* then `option.key` is used instead of `option.value` in place of `<VALUE>`.
*/
id: PropTypes.string,
/**
* If `false`, the label will be visually hidden (but remains accessible by assistive
* technologies).
*
* Automatically set to `false` when the component is rendered within `InputGroup` component.
*/
isLabelVisible: PropTypes.bool,
/**
* Select field label.
*/
label: PropTypes.node.isRequired,
/**
* Layout of the field.
*
* Ignored if the component is rendered within `FormLayout` component
* as the value is inherited in such case.
*/
layout: PropTypes.oneOf(['horizontal', 'vertical']),
/**
* Set of options to be chosen from.
*
* Either set of individual or grouped options is acceptable.
*
* For generating unique IDs the `option.value` is normally used. For cases when this is not practical or
* the `option.value` values are not unique the `option.key` attribute can be set manually.
* The same applies for the `label` value of grouped options which is supposed to be unique.
* To ensure uniqueness `key` attribute can be set manually.
*/
options: PropTypes.oneOfType([
PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string,
label: PropTypes.string.isRequired,
options: PropTypes.arrayOf(PropTypes.shape({
disabled: PropTypes.bool,
key: PropTypes.string,
label: PropTypes.string.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
})),
}),
),
PropTypes.arrayOf(PropTypes.shape({
disabled: PropTypes.bool,
key: PropTypes.string,
label: PropTypes.string.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
})),
]).isRequired,
/**
* If `true`, the input will be rendered as if it was required.
*/
renderAsRequired: PropTypes.bool,
/**
* If `true`, the input will be made and rendered as required, regardless of the `renderAsRequired` prop.
*/
required: PropTypes.bool,
/**
* Size of the field.
*
* Ignored if the component is rendered within `InputGroup` component as the value is inherited in such case.
*/
size: PropTypes.oneOf(['small', 'medium', 'large']),
/**
* Alter the field to provide feedback based on validation result.
*/
validationState: PropTypes.oneOf(['invalid', 'valid', 'warning']),
/**
* Validation message to be displayed.
*
* Validation text is never rendered when the component is placed into `InputGroup`. Instead, the `InputGroup`
* component itself renders all validation texts of its nested components.
*/
validationText: PropTypes.node,
/**
* Design variant of the field, further customizable with CSS custom properties.
*/
variant: PropTypes.oneOf(['filled', 'outline']),
};
export const SelectFieldWithGlobalProps = withGlobalProps(SelectField, 'SelectField');
export default SelectFieldWithGlobalProps;