import React , { useState } from 'react' ; import PropTypes from 'prop-types' ; import { css , cx } from '@leafygreen-ui/emotion' ; import CheckmarkIcon from '@leafygreen-ui/icon/dist/Checkmark' ; import CheckmarkWithCircleIcon from '@leafygreen-ui/icon/dist/CheckmarkWithCircle' ; import WarningIcon from '@leafygreen-ui/icon/dist/Warning' ; import InteractionRing from '@leafygreen-ui/interaction-ring' ; import { uiColors } from '@leafygreen-ui/palette' ; import { createDataProp , HTMLElementProps , Either } from '@leafygreen-ui/lib' ; import { useIdAllocator , useValidation } from '@leafygreen-ui/hooks' ; import { Description , Label } from '@leafygreen-ui/typography' ; const iconSelectorProp = createDataProp ( 'icon-selector' ) ; export const State = { None : 'none' , Valid : 'valid' , Error : 'error' , } as const ; export type State = typeof State [ keyof typeof State ] ; export const TextInputType = { Email : 'email' , Password : 'password' , Search : 'search' , Text : 'text' , Url : 'url' , Tel : 'tel' , Number : 'number' , } as const ; export type TextInputType = typeof TextInputType [ keyof typeof TextInputType ] ; const Mode = { Light : 'light' , Dark : 'dark' , } as const ; type Mode = typeof Mode [ keyof typeof Mode ] ; export const SizeVariant = { XSmall : 'xsmall' , Small : 'small' , Default : 'default' , Large : 'large' , } as const ; export type SizeVariant = typeof SizeVariant [ keyof typeof SizeVariant ] ; export const BaseFontSize = 14 | 16 ; export type BaseFontSize = typeof BaseFontSize ; interface TextInputProps extends HTMLElementProps < 'input' , HTMLInputElement > { /**
     * id associated with the TextInput component.
     */ id ? : string ; /**
     * Text shown in bold above the input element.
     */ label ? : string | null ; /**
     * Text that gives more detail about the requirements for the input.
     */ description ? : string ; /**
     * Whether or not the field is optional.
     * Default: false
     */ optional ? : boolean ; /**
     * Whether or not the field is currently disabled.
     * Default: false
     */ disabled ? : boolean ; /**
     * Callback to be executed when the value of the input field changes.
     */ onChange ? : ( e : React . ChangeEvent < HTMLInputElement > ) => void ; /**
     * The placeholder text shown in the input field before the user begins typing.
     */ placeholder ? : string ; /**
     * The message shown below the input field if the value is invalid.
     */ errorMessage ? : string ; /**
     * The current state of the TextInput. This can be none, valid, or error.
     */ state ? : State ; /**
     * The current value of the input field. If a value is passed to this prop, component will be controlled by consumer.
     */ value ? : string ; /**
     * className supplied to the TextInput container.
     */ className ? : string ; /**
     *  determines whether or not the component appears in dark mode.
     */ darkMode ? : boolean ; type ? : TextInputType ; handleValidation ? : ( value : string ) => void ; [ 'aria-labelledby' ] ? : string ; /**
     *  determines the font size and padding.
     */ sizeVariant ? : SizeVariant ; /**
     *  determines the base font size if sizeVariant is set to default.
     */ baseFontSize ? : BaseFontSize ; } type AriaLabels = 'label' | 'aria-labelledby' ; type AccessibleTextInputProps = Either < TextInputProps , AriaLabels > | ( TextInputProps & { type : 'search' ; 'aria-label' : string ; } ) ; const interactionRingStyle = css `
  width: 100%;
` ; const textInputStyle = css `
  display: flex;
  flex-direction: column;
` ; const inputContainerStyle = css `
  position: relative;
  display: flex;
  align-items: center;
  z-index: 0;
` ; const inputStyle = css `
  width: 100%;
  height: 36px;
  border-radius: 4px;
  padding-left: 12px;
  font-size: 14px;
  font-weight: normal;
  font-family: Akzidenz, ‘Helvetica Neue’, Helvetica, Arial, sans-serif;
  border: 1px solid;
  transition: border-color 150ms ease-in-out;
  z-index: 1;
  outline: none;

  &::placeholder {
    color: ${ uiColors . gray . base };
  }

  &:disabled {
    cursor: not-allowed;
  }
` ; const inputIconStyle = css `
  position: absolute;
  display: flex;
  align-items: center;
  right: 12px;
  z-index: 1;
` ; const validIconStyle = css `
  color: ${ uiColors . green . base };
` ; const optionalStyle = css `
  color: ${ uiColors . gray . dark1 };
  font-size: 12px;
  font-style: italic;
  font-weight: normal;
` ; const errorMessageStyle = css `
  font-size: 14px;
  min-height: 20px;
  padding-top: 4px;
  font-weight: normal;
` ; interface ColorSets { inputColor : string ; inputBackgroundColor : string ; disabledColor : string ; disabledBackgroundColor : string ; errorIconColor : string ; errorBorder : string ; errorMessage : string ; optional : string ; defaultBorder : string ; validBorder : string ; } const colorSets = { [ Mode . Light ] : { inputColor : uiColors . gray . dark3 , inputBackgroundColor : uiColors . white , disabledColor : uiColors . gray . base , disabledBackgroundColor : uiColors . gray . light2 , errorIconColor : uiColors . red . base , errorMessage : uiColors . red . base , errorBorder : uiColors . red . base , optional : uiColors . gray . dark1 , defaultBorder : uiColors . gray . base , validBorder : uiColors . green . base , } , [ Mode . Dark ] : { inputColor : uiColors . white , inputBackgroundColor : '#394F5A' , disabledColor : uiColors . gray . dark1 , disabledBackgroundColor : '#263843' , errorIconColor : '#EF8D6F' , errorMessage : '#EF8D6F' , errorBorder : '#5a3c3b' , optional : uiColors . gray . light1 , defaultBorder : '#394F5A' , validBorder : '#394F5A' , } , } as const ; const interactionRingColor = { [ Mode . Light ] : { [ State . Error ] : uiColors . red . light3 , [ State . Valid ] : uiColors . green . light3 , } , [ Mode . Dark ] : { [ State . Error ] : uiColors . red . dark2 , [ State . Valid ] : uiColors . gray . dark1 , } , } ; interface SizeSet { inputHeight : number ; inputText : number ; text : number ; lineHeight : number ; padding : number ; } function getStatefulInputStyles ( { state , optional , mode , disabled , sizeSet , } ) { switch ( state ) { case State . Valid : { return css `
        padding-right: 30px;
        border-color: ${ ! disabled ? colorSets [ mode ] . validBorder : 'inherit' };
      ` ; } case State . Error : { return cx ( css `
          padding-right: 30px;
          border-color: ${ ! disabled ? colorSets [ mode ] . errorBorder : 'inherit' };
        ` , { [ css `
            background-color: #5a3c3b;
          ` ] : mode === Mode . Dark , } ) ; } default : { return css `
        padding-right: ${ optional ? 60 : sizeSet . padding }px;
        border-color: ${ colorSets [ mode ] . defaultBorder };
      ` ; } } } function getSizeSets ( baseFontSize , sizeVariant ) { const sizeSets = { [ SizeVariant . XSmall ] : { inputHeight : 22 , inputText : 12 , text : 14 , lineHeight : 20 , padding : 10 , } , [ SizeVariant . Small ] : { inputHeight : 28 , inputText : 14 , text : 14 , lineHeight : 20 , padding : 10 , } , [ SizeVariant . Default ] : { inputHeight : 36 , inputText : baseFontSize , text : baseFontSize , lineHeight : 20 , padding : 12 , } , [ SizeVariant . Large ] : { inputHeight : 48 , inputText : 18 , text : 18 , lineHeight : 22 , padding : 16 , } , } ; return sizeSets [ sizeVariant ] ; } param props . id param props . label param props . description param props . optional param props . disabled param props . onChange param props . placeholder param props . errorMessage param props . state param props . value param props . className param props . darkMode param props . sizeVariant const TextInput = React . forwardRef ( ( { label , description , onChange , placeholder , errorMessage , optional = false , disabled = false , state = State . None , type = TextInputType . Text , id : propsId , value : controlledValue , className , darkMode = false , sizeVariant = SizeVariant . Default , 'aria-labelledby' : ariaLabelledby , handleValidation , baseFontSize = 14 , ... rest } , forwardRef ) => { const mode = darkMode ? Mode . Dark : Mode . Light ; const isControlled = typeof controlledValue === 'string' ; const [ uncontrolledValue , setValue ] = useState ( '' ) ; const value = isControlled ? controlledValue : uncontrolledValue ; const id = useIdAllocator ( { prefix : 'textinput' , id : propsId } ) ; const sizeSet = getSizeSets ( baseFontSize , sizeVariant ) ; const validation = useValidation < HTMLInputElement > ( handleValidation ) ; function onValueChange ( e ) { if ( onChange ) { onChange ( e ) ; } if ( ! isControlled ) { setValue ( e . target . value ) ; } validation . onChange ( e ) ; } if ( type !== 'search' && ! label && ! ariaLabelledby ) { console . error ( 'For screen-reader accessibility, label or aria-labelledby must be provided to TextInput.' ) ; } if ( type === 'search' && ! rest [ 'aria-label' ] ) { console . error ( 'For screen-reader accessibility, aria-label must be provided to TextInput.' ) ; } const RenderedCheckmarkIcon = darkMode ? CheckmarkWithCircleIcon : CheckmarkIcon ; return ( < div className = { cx ( textInputStyle , className ) } > { label && ( < Label darkMode = { darkMode } htmlFor = { id } disabled = { disabled } className = { cx ( css `
              font-size: ${ sizeSet . text } px ; `)}>
            {label}
          </Label>)}
        {description && (<Description darkMode={darkMode} className={cx(css ` font - size : $ { sizeSet . text } px ; line - height : $ { sizeSet . lineHeight } px ; `)}>
            {description}
          </Description>)}
        <div className={inputContainerStyle}>
          <InteractionRing className={interactionRingStyle} darkMode={darkMode} disabled={disabled} ignoreKeyboardContext={true} color={state === State.Valid || state === State.Error
            ? {
                hovered: interactionRingColor[mode][state],
            }
            : undefined}>
            <input {...rest} aria-labelledby={ariaLabelledby} type={type} className={cx(inputStyle, css ` color : $ { colorSets [ mode ] . inputColor } ; background - color : $ { colorSets [ mode ] . inputBackgroundColor } ; font - size : $ { sizeSet . inputText } px ; height : $ { sizeSet . inputHeight } px ; padding - left : $ { sizeSet . padding } px ; & : focus { border : 1 px solid $ { colorSets [ mode ] . inputBackgroundColor } ; } & : disabled { color : $ { colorSets [ mode ] . disabledColor } ; background - color : $ { colorSets [ mode ] . disabledBackgroundColor } ; & : - webkit - autofill { & , & : hover , & : focus { appearance : none ; border : 1 px solid $ { colorSets [ mode ] . defaultBorder } ; - webkit - text - fill - color : $ { colorSets [ mode ] . disabledColor } ; - webkit - box - shadow : 0 0 0 px 1000 px $ { colorSets [ mode ] . disabledBackgroundColor } inset ; } } } `, getStatefulInputStyles({
            state,
            optional,
            mode,
            disabled,
            sizeSet,
        }))} value={value} required={!optional} disabled={disabled} placeholder={placeholder} onChange={onValueChange} onBlur={validation.onBlur} ref={forwardRef} id={id} autoComplete={disabled ? 'off' : rest?.autoComplete || 'on'} aria-invalid={state === 'error'}/>
          </InteractionRing>

          <div {...iconSelectorProp.prop} className={inputIconStyle}>
            {state === State.Valid && (<RenderedCheckmarkIcon role="presentation" className={validIconStyle}/>)}

            {state === State.Error && (<WarningIcon role="presentation" className={css ` color : $ { colorSets [ mode ] . errorIconColor } ; `}/>)}

            {state === State.None && optional && (<div className={cx(optionalStyle, css ` color : $ { colorSets [ mode ] . optional } ; `)}>
                <p>Optional</p>
              </div>)}
          </div>
        </div>
        {state === State.Error && errorMessage && (<div className={cx(errorMessageStyle, css ` color : $ { colorSets [ mode ] . errorMessage } ; font - size : $ { sizeSet . text } px ; line - height : $ { sizeSet . lineHeight } px ; `)}>
            <label>{errorMessage}</label>
          </div>)}
      </div>);
});
TextInput.displayName = 'TextInput';
TextInput.propTypes = {
    id: PropTypes.string,
    label: PropTypes.string,
    description: PropTypes.string,
    optional: PropTypes.bool,
    disabled: PropTypes.bool,
    onChange: PropTypes.func,
    placeholder: PropTypes.string,
    errorMessage: PropTypes.string,
    state: PropTypes.oneOf(Object.values(State)),
    value: PropTypes.string,
    className: PropTypes.string,
    sizeVariant: PropTypes.oneOf(Object.values(SizeVariant)),
    baseFontSize: PropTypes.oneOf([14, 16]),
};
export default TextInput;
