UNPKG

@react-ui-org/react-ui

Version:

React UI is a themeable UI library for React apps.

265 lines (257 loc) 8.36 kB
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;