import { generateGuid } from '@vev/utils'; import { isString, upperFirst } from 'lodash'; import React, { Children, HTMLAttributes, InputHTMLAttributes, ReactNode, useEffect, useMemo, useRef, } from 'react'; import { useUniqId } from '../../hooks'; import { useFormField } from '../../hooks/form'; import { FormFieldProps } from '../silke-form'; import { SilkeTextFieldItem } from './silke-text-field-item'; import { SilkeTextFieldOutline, SilkeTextFieldOutlineProps } from './silke-text-field-outline'; import styles from './silke-text-field.scss'; import { useTextFieldContext } from './text-field-context'; export type SilkeInputSize = 's' | 'base'; type BaseProps = FormFieldProps & SilkeTextFieldOutlineProps & { autocomplete?: boolean; autoFocus?: boolean; /** Field required input (if string the string will be used as error msg) */ required?: boolean | string; maxLength?: number; minLength?: number; type?: string; }; type OmitAttrs = | 'name' | 'value' | 'size' | 'kind' | 'children' | 'required' | 'onChange' | 'onFocus' | 'readonly' | 'onBlur'; type MultilineProps = { multiline: boolean | number | undefined; type?: string; } & BaseProps & Omit, OmitAttrs>; export type SingleLineProps = { multiline?: undefined | false; } & BaseProps & Omit, OmitAttrs>; type SingleLineNumberProps = { multiline?: undefined | false; type: 'number' } & BaseProps & Omit, OmitAttrs>; export type SilkeTextFieldProps = SingleLineProps | MultilineProps | SingleLineNumberProps; const validateText = (props: SilkeTextFieldProps, value: string | number | undefined) => { if (props.required && !value) return isString(props.required) ? props.required : 'Please fill out this field'; if (isString(value) && props.minLength && (value.length || 0) < props.minLength) return `This field must be at least ${props.minLength} characters`; }; export const SilkeTextField = React.forwardRef( (props, ref) => { const { autoFocus, autocomplete, children, className, disabled, error, flex, help, inline, kind, multiline, name, onChange, onError, placeholder, readOnly, size, tooltip, validate, validator, value, width, label = upperFirst(name), ...rest } = useFormField(props, validateText); const context = useTextFieldContext(); const inputRef = useRef(null); const helpId = useMemo(() => { return `help-${generateGuid()}`; }, []); const uid = useUniqId('silke-input'); const inputId = rest.id || uid; const { type } = rest as Omit, OmitAttrs>; useEffect(() => { if (autoFocus && inputRef && inputRef.current) { const inputField = inputRef.current; // Need to wrap this in setTimeout to increase the chance of it being focused when inside a popover setTimeout(() => { inputField.focus(); // For some strange reason, you can't do range select on email and password fields if (!type || type === 'text') inputField.setSelectionRange(0, inputField.value.length); }, 0); } }, [autoFocus]); const before: ReactNode[] = []; const after: ReactNode[] = []; Children.forEach(children, (child) => { if (React.isValidElement(child) && child.type === SilkeTextFieldItem && child.props.before) { before.push(child); } else { after.push(child); } }); const inputStyle = { ...rest.style }; if (width) inputStyle.width = width; const maxLengthHelp = rest.maxLength && isString(value) && `${value?.length || 0}/${rest.maxLength}`; return ( {before} {React.createElement(multiline ? 'textarea' : 'input', { ...rest, disabled, readOnly: readOnly || context.readOnly, ref: inputRef, className: multiline ? styles.input + ' ' + styles.inputMultiline : styles.input, placeholder, id: inputId, name, style: inputStyle, value: value ?? '', rows: multiline === true ? 2 : multiline || 2, autoComplete: autocomplete ? 'on' : 'off', ...(autoFocus && { autofocus: 'true' }), 'aria-invalid': !!error, 'aria-describedby': help ? helpId : undefined, onChange: (event: React.ChangeEvent) => { onChange(event.target.value); }, })} {after} ); }, ); SilkeTextField.displayName = 'SilkeTextField';