monday-ui-react-core
Version:
Official monday.com UI resources for application development in React.js
198 lines (177 loc) • 5.8 kB
JSX
import React, { useRef, useState, useCallback, useEffect, useMemo } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { useButton } from "@react-aria/button";
import Heading from "../Heading/Heading";
import EditableInput, { TEXTAREA_TYPE } from "../EditableInput/EditableInput";
import { TYPES } from "../Heading/HeadingConstants";
import { SIZES } from "../../constants/sizes";
import usePrevious from "../../hooks/usePrevious";
import "./EditableHeading.scss";
const EditableHeading = props => {
const {
id,
className,
value,
editing,
disabled,
onFinishEditing,
onCancelEditing,
errorClassTimeout,
style,
onStartEditing,
contentRenderer
} = props;
// 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(() => {
if (disabled || isEditing) return;
setIsEditing(true);
onStartEditing && onStartEditing();
}, [isEditing, disabled, setIsEditing, onStartEditing]);
const onFinishEditingCallback = useCallback(
(newValue, event) => {
setIsEditing(false);
setValueState(newValue);
onFinishEditing && onFinishEditing(newValue, event);
},
[onFinishEditing, setIsEditing, setValueState]
);
const onCancelEditingCallback = useCallback(
event => {
setIsEditing(false);
onCancelEditing && onCancelEditing(event);
},
[onCancelEditing, setIsEditing]
);
const clearErrorState = useCallback(() => {
setIsError(false);
}, [setIsError]);
const onInputErrorCallback = useCallback(() => {
setIsError(true);
}, [setIsError]);
const onInputSuccessCallback = useCallback(() => {
clearErrorState();
}, [clearErrorState]);
const { buttonProps } = useButton({
onPress: onClick,
elementType: "div"
});
// Effects
useEffect(() => {
// Update value if changed from props
if (!editing && value !== prevValue && value !== valueState) {
setValueState(value);
}
}, [editing, value, prevValue, valueState, setValueState]);
useEffect(() => {
let timer;
if (isError) {
timer = setTimeout(clearErrorState, errorClassTimeout);
}
return () => timer && clearTimeout(timer);
}, [isError, setIsError, clearErrorState, errorClassTimeout]);
// Render
const getContentProps = () => {
const suggestEditOnHover = props.suggestEditOnHover && !disabled;
const valueOrPlaceholder = valueState || props.placeholder || "";
return {
value: props.displayPlaceholderInTextMode ? valueOrPlaceholder : valueState,
type: props.type,
suggestEditOnHover,
tooltipPosition: props.tooltipPosition,
ellipsisMaxLines: props.ellipsisMaxLines,
nonEllipsisTooltip: props.tooltip,
size: props.size
};
};
const renderContentComponent = () => {
const contentProps = getContentProps();
if (contentRenderer) {
return contentRenderer(contentProps);
}
// eslint-disable-next-line react/jsx-props-no-spreading
return <Heading {...contentProps} />;
};
const getInputProps = () => {
const textAreaType = props.ellipsisMaxLines > 1 ? TEXTAREA_TYPE : undefined;
const inputType = props.inputType || textAreaType;
return {
value: valueState,
className: `editable-heading-input element-type-${props.type} size-${props.size}`,
isValidValue: props.isValidValue,
onChange: props.onChange,
onKeyDown: props.onKeyDown,
onClick: props.onClick,
onTabHandler: props.onTabHandler,
onArrowKeyDown: props.onArrowKeyDown,
autoComplete: props.autoComplete,
maxLength: props.maxLength,
placeholder: props.placeholder,
shouldFocusOnMount: props.shouldFocusOnMount,
selectOnMount: props.selectOnMount,
inputType,
ignoreBlurClass: props.ignoreBlurClass,
autoSize: props.autoSize,
textareaSubmitOnEnter: props.textareaSubmitOnEnter,
onFinishEditing: onFinishEditingCallback,
onCancelEditing: onCancelEditingCallback,
onError: onInputErrorCallback,
onSuccess: onInputSuccessCallback,
ariaLabel: props.inputAriaLabel
};
};
const renderInputComponent = () => {
const inputProps = getInputProps();
// eslint-disable-next-line react/jsx-props-no-spreading
return <EditableInput {...inputProps} />;
};
const clickProps = useMemo(() => {
return isEditing ? { onClick, role: "button", tabIndex: "0" } : buttonProps;
}, [isEditing, buttonProps, onClick]);
return (
<div
ref={ref}
style={style}
className={cx("editable-heading--wrapper", className)}
{...clickProps}
aria-label={`${value} ${props.tooltip || ""}`}
id={id}
>
{isEditing ? renderInputComponent() : renderContentComponent()}
</div>
);
};
EditableHeading.propTypes = {
className: PropTypes.string,
id: PropTypes.string,
type: PropTypes.oneOf(Object.keys(TYPES)),
errorClass: PropTypes.string,
errorClassTimeout: PropTypes.number,
displayPlaceholderInTextMode: PropTypes.bool,
suggestEditOnHover: PropTypes.bool,
autoSize: PropTypes.bool,
size: PropTypes.oneOf(Object.values(SIZES)),
inputAriaLabel: PropTypes.string
};
EditableHeading.defaultProps = {
className: "",
id: "",
type: TYPES.H1,
errorClass: "error",
errorClassTimeout: 2000,
displayPlaceholderInTextMode: true,
suggestEditOnHover: true,
autoSize: true,
size: SIZES.LARGE,
inputAriaLabel: undefined
};
EditableHeading.types = TYPES;
EditableHeading.sizes = SIZES;
export default EditableHeading;