import { Form, Formik, FormikConfig } from 'formik'; import { noop } from 'lodash'; import { ReactNode, useEffect, useRef, useState, useCallback } from 'react'; import { usePrevious } from 'react-use'; import { useId } from '@reach/auto-id'; import * as Yup from 'yup'; import { useTheme } from 'styled-components'; import { TXProp } from '../theme'; import { Box } from '../Box'; import { Button } from '../Button'; import { Text, TextProps } from '../Text'; import { EDITABLE_TEXT_FIELD_NAME, EditableTextFormikValues } from './types'; import { EditableTextTextarea } from './EditableTextTextarea'; const EditableTextSchema = Yup.object().shape({ [EDITABLE_TEXT_FIELD_NAME]: Yup.string().trim(), }); const RequiredEditableTextSchema = Yup.object().shape({ [EDITABLE_TEXT_FIELD_NAME]: Yup.string().trim().required('This field is required'), }); export type EditableTextFormValues = { [EDITABLE_TEXT_FIELD_NAME]: string; }; export interface EditableTextProps extends Pick, Pick, 'onSubmit'> { 'data-testid'?: string; behavior?: 'box' | 'text'; disabled?: boolean; formatValue?: (children: ReactNode) => ReactNode; isEditing?: boolean; id?: string; label: string; /** * This will set the max character length for the textarea. */ maxLength?: number; onEditingStateChange?: (isEditingState: boolean) => void; onReset?: () => void; placeholder?: string; readOnly?: boolean; tx?: TXProp & { EditableTextButton?: TXProp; EditableTextText?: TXProp; EditableTextTextarea?: TXProp; }; value: string; error?: string; onValueChange?: (value: string) => void; required?: boolean; } export const EditableText = ({ as, behavior = 'box', ['data-testid']: testId, disabled, formatValue = (value) => value, isEditing = false, id, label, maxLength, onEditingStateChange = noop, onReset = noop, onSubmit = noop, placeholder, readOnly, tx = {}, value = '', variant = 'text-ui-16', error, onValueChange, required, }: EditableTextProps) => { const theme = useTheme(); const autoId = useId(id)!; const [isEditingState, setIsEditingState] = useState(isEditing); const isPreviousIsEditing = usePrevious(isEditingState); const buttonRef = useRef(null); const textareaRef = useRef(null); const isTextBehavior = behavior === 'text'; const { EditableTextButton: buttonStyles, EditableTextText: textStyles, EditableTextTextarea: textareaStyles, ...containerStyles } = tx; const setIsEditing = useCallback( (isEditing: boolean) => { setIsEditingState(isEditing); onEditingStateChange(isEditing); }, [onEditingStateChange], ); useEffect(() => { if (isEditingState && textareaRef?.current) { textareaRef.current?.focus(); textareaRef.current.setSelectionRange( textareaRef.current.value.length, textareaRef.current.value.length, ); } if (!isEditingState && isPreviousIsEditing && buttonRef?.current) { buttonRef.current.focus(); } }, [buttonRef, isEditingState, isPreviousIsEditing, textareaRef]); return ( enableReinitialize onReset={() => { onReset(); setIsEditing(false); }} validateOnChange={false} initialValues={{ [EDITABLE_TEXT_FIELD_NAME]: value }} onSubmit={async (values, formikHelpers) => { /** * Only submit the form if the value has changed. */ const formikError = await formikHelpers.validateForm(); const fieldError = formikError[EDITABLE_TEXT_FIELD_NAME]; const rawValue = values[EDITABLE_TEXT_FIELD_NAME]; const trimmedValue = rawValue.trim(); const trimmedValues: EditableTextFormikValues = { [EDITABLE_TEXT_FIELD_NAME]: trimmedValue, }; formikHelpers.setFieldValue(EDITABLE_TEXT_FIELD_NAME, trimmedValue); if (!fieldError) { setIsEditing(false); onSubmit(trimmedValues, formikHelpers); } }} validationSchema={required ? RequiredEditableTextSchema : EditableTextSchema} > {({ values, errors }) => { const formError = error ?? errors[EDITABLE_TEXT_FIELD_NAME]; return ( {isEditingState && ( )} {isEditingState && !!formError && ( {formError} )} ); }} ); };