import { camelCase } from "lodash-es"; import { getStyle } from "../../helpers/typesciptCssModulesHelper"; import { ComponentDefaultTestId, getTestId } from "../../tests/test-ids-utils"; import cx from "classnames"; import React, { useCallback, useEffect, useRef, useState } from "react"; import useIsomorphicLayoutEffect from "../../hooks/ssr/useIsomorphicLayoutEffect"; import Heading, { HeadingProps } from "../LegacyHeading/LegacyHeading"; import Clickable from "../Clickable/Clickable"; import EditableInput, { EditableInputProps } from "../EditableInput/EditableInput"; import usePrevious from "../../hooks/usePrevious"; import { InputType } from "../EditableInput/EditableInputConstants"; import { HeadingSizes, HeadingTypes } from "../LegacyHeading/LegacyHeadingConstants"; import { Sizes } from "../../constants"; import { withStaticProps } from "../../types"; import headingStyles from "../LegacyHeading/LegacyHeading.module.scss"; import styles from "./LegacyEditableHeading.module.scss"; import { backwardCompatibilityForProperties } from "../../helpers/backwardCompatibilityForProperties"; export interface EditableHeadingProps extends EditableInputProps, HeadingProps { displayPlaceholderInTextMode?: boolean; inputAriaLabel?: string; errorClass?: string; headingClassName?: string; inputClassName?: string; /** * @deprecated - use "data-testid" instead */ dataTestId?: string; "data-testid"?: string; editing?: boolean; disabled?: boolean; errorClassTimeout?: number; style?: React.CSSProperties; onStartEditing?: (event: React.KeyboardEvent) => void; tooltip?: string; insetFocus?: boolean; contentRenderer?: React.FC; } /** * @deprecated - use EditableHeading from 'monday-ui-react-core/next' */ const LegacyEditableHeading: React.FC & { sizes?: typeof Sizes; types?: typeof HeadingTypes; } = props => { const { id = "", className, inputClassName = "", dataTestId: backwardCompatabilityDataTestId = "", "data-testid": dataTestId = "", value, editing, disabled, onFinishEditing, onCancelEditing, onIgnoreBlurEvent, errorClassTimeout = 2000, style, customColor, onStartEditing, contentRenderer, tooltip, autoSize = true, highlightTerm = null, insetFocus = false, size = HeadingSizes.LARGE, displayPlaceholderInTextMode = true, suggestEditOnHover = true, type = Heading.types.h1 } = props; const overrideDataTestId = backwardCompatibilityForProperties([dataTestId, backwardCompatabilityDataTestId]); // State const [isEditing, setIsEditing] = useState(editing && !disabled); const [isError, setIsError] = useState(false); const [valueState, setValueState] = useState(value || ""); const prevValue = usePrevious(value); // Refs const ref = useRef(null); // Callbacks const onClick = useCallback( (event: React.KeyboardEvent) => { if (disabled || isEditing) return; setIsEditing(true); onStartEditing && onStartEditing(event); }, [isEditing, disabled, setIsEditing, onStartEditing] ); const onFinishEditingCallback = useCallback( (newValue: string, event: React.KeyboardEvent | React.FocusEvent) => { setIsEditing(false); setValueState(newValue); onFinishEditing?.(newValue, event); }, [onFinishEditing, setIsEditing, setValueState] ); const onCancelEditingCallback = useCallback( (event: React.KeyboardEvent) => { setIsEditing(false); onCancelEditing?.(event); }, [onCancelEditing, setIsEditing] ); const onIgnoreBlurEventCallback = useCallback( (value: string) => { onIgnoreBlurEvent?.(value); }, [onIgnoreBlurEvent] ); const clearErrorState = useCallback(() => { setIsError(false); }, [setIsError]); const onInputErrorCallback = useCallback(() => { setIsError(true); }, [setIsError]); const onInputSuccessCallback = useCallback(() => { clearErrorState(); }, [clearErrorState]); // Effects useEffect(() => { // Update value if changed from props if (!editing && value !== prevValue && value !== valueState) { setValueState(value); } }, [editing, value, prevValue, valueState, setValueState]); useEffect(() => { // update isEditing state if "editing" prop changed setIsEditing(editing); }, [editing]); useIsomorphicLayoutEffect(() => { if (!editing && !valueState && value) { // When user entered empty value - rollback to value from props setValueState(value); } }, [editing, value, prevValue, valueState, setValueState]); useEffect(() => { let timer: ReturnType; if (isError) { timer = setTimeout(clearErrorState, errorClassTimeout); } return () => timer && clearTimeout(timer); }, [isError, setIsError, clearErrorState, errorClassTimeout]); const renderContentComponent = () => { const valueOrPlaceholder = valueState || props.placeholder || ""; const contentProps = { value: displayPlaceholderInTextMode ? valueOrPlaceholder : valueState, type, customColor, suggestEditOnHover: suggestEditOnHover && !disabled, tooltipPosition: props.tooltipPosition, ellipsisMaxLines: props.ellipsisMaxLines, nonEllipsisTooltip: props.tooltip, size: size as Sizes, highlightTerm, className: cx(styles.headingComponent, props.headingClassName) }; if (contentRenderer) { return contentRenderer(contentProps); } // eslint-disable-next-line react/jsx-props-no-spreading return ; }; const getInputProps = () => { const textAreaType = props.ellipsisMaxLines > 1 ? InputType.TEXT_AREA : undefined; const inputType = props.inputType || textAreaType; return { value: valueState, className: cx( getStyle(headingStyles, camelCase("element-type-" + type)), getStyle(headingStyles, camelCase("size-" + size)), inputClassName ), isValidValue: props.isValidValue, onChange: props.onChange, onKeyDown: props.onKeyDown, onKeyPress: props.onKeyPress, onClick: props.onClick, customColor, onTabHandler: props.onTabHandler, onArrowKeyDown: props.onArrowKeyDown, autoComplete: props.autoComplete, maxLength: props.maxLength, placeholder: props.placeholder, shouldFocusOnMount: props.shouldFocusOnMount, selectOnMount: props.selectOnMount, onBlur: props.onBlur, onFocus: props.onFocus, inputType, ignoreBlurClass: props.ignoreBlurClass, autoSize, textareaSubmitOnEnter: props.textareaSubmitOnEnter, onFinishEditing: onFinishEditingCallback, onCancelEditing: onCancelEditingCallback, onIgnoreBlurEvent: onIgnoreBlurEventCallback, onError: onInputErrorCallback, onSuccess: onInputSuccessCallback, ariaLabel: props.inputAriaLabel }; }; const renderInputComponent = () => { const inputProps = getInputProps(); // eslint-disable-next-line react/jsx-props-no-spreading return ; }; const shouldEdit = !disabled && isEditing; return (
{shouldEdit ? renderInputComponent() : renderContentComponent()}
); }; export default withStaticProps(LegacyEditableHeading, { types: HeadingTypes, sizes: HeadingSizes });