Source

stories/AnyFieldLabel.jsx

/** @module AnyFieldLabel */
import React from 'react';
import PropTypes from 'prop-types';

import TooltipIcon from './TooltipIcon';

import {
  InputLabel, Box, FormLabel, FormHelperText, useTheme
} from '@mui/material';
import RequiredIndicator from './RequiredIndicator';

/**
 * Icons for info icons
 * @typedef {Object} FieldIconOptions
 * @property {string} [color] - the color of the icon
 * @property {string} [gap] - the gap between the label and the icon
 * @property {boolean} [beforeLabel] - whether to display the icon before the label
 * @property {React.Component} [iconComponent] - a component to use instead of the default InfoIcon
 * @property {string} [iconText] - the text to display in the info icon
 */

/**
 * Theme group for the label
 * @typedef {Object} FieldLabelThemeGroup
 * @property {object} [anyFieldLabel] - any theme properties to use for the box containing the label
 * @property {object} [anyFieldLabel.helperText] - any theme properties to use for the helper text
 */

/**
 * Various options for the fields
 * @typedef {Object} FieldOptions
 * @property {FieldIconOptions} [icon] - the options to pass to the info icon
 * @property {FieldLabelThemeGroup} [labelThemeGroup] - the theme group to use for the label
 */

/**
 * AnyField is a wrapper around the various field types that implements the react-hook-form Controller
 * @function
 * @param {object} props
 * @param {string} props.htmlFor - the id to use for htmlFor
 * @param {string} [props.error] - the error message to display
 * @param {boolean} [props.disabled] - is the field disabled
 * @param {boolean} [props.required] - is the field required
 * @param {string} [props.label] - the label to display
 * @param {string} [props.asFormInput] - whether to render the label using FormLabel or InputLabel
 * @param {string} [props.iconText] - the text to display in the info icon
 * @param {string} [props.helperText] - the text to display in the helper text
 * @param {FieldOptions} [props.fieldOptions] - the options to pass to the field
 * @returns {React.ReactElement} a label for a field with an optional info icon and required indicator
 */
const AnyFieldLabel = ({ htmlFor, error, disabled, required, label, iconText, helperText, asFormInput = false, fieldOptions = {}, className, sx }) => {
  const theme = useTheme();

  // Attempt to use the themeGroup from props, then the anyFieldLabel defined in the base theme
  const { anyFieldLabel } = theme;
  const { themeGroup } = fieldOptions || {};
  const gAFL = themeGroup?.anyFieldLabel || anyFieldLabel || null;
  let gHelper = gAFL?.helperText || anyFieldLabel?.helperText || { marginTop: 0 };

  const BoxSx = {
    display: 'flex',
    alignItems: 'center',
    gap: fieldOptions?.icon?.gap || '0.5rem',
    ...sx
  };

  if (iconText && fieldOptions?.icon?.beforeLabel) {
    sx.flexDirection = 'row-reverse';
    sx.justifyContent = 'flex-end';
  }

  const iconProps = fieldOptions?.icon || { color: 'primary' };

  const labelComponent = asFormInput ? (
    <FormLabel className={className} component="legend"><RequiredIndicator isRequired={!!required} />{label}</FormLabel>
  ) : (
    <InputLabel className={className} htmlFor={htmlFor} error={!!error}><RequiredIndicator disabled={disabled} isRequired={!!required} />
      {label}
    </InputLabel>
  );

  return (
    <Box sx={gAFL}>
      <Box sx={BoxSx}>
        {labelComponent}
        {iconText && <TooltipIcon infoText={iconText} {...iconProps} />}
      </Box>
      {helperText && <FormHelperText sx={gHelper}>{helperText}</FormHelperText>}
    </Box>
  );
};

AnyFieldLabel.propTypes = {
  htmlFor: PropTypes.string.isRequired,
  className: PropTypes.string,
  sx: PropTypes.object,
  asFormInput: PropTypes.bool,
  error: PropTypes.bool,
  disabled: PropTypes.bool,
  required: PropTypes.bool,
  label: PropTypes.string.isRequired,
  iconText: PropTypes.string,
  helperText: PropTypes.string,
  fieldOptions: PropTypes.object
};

export default AnyFieldLabel;