UNPKG

@cimpress/react-components

Version:
180 lines 9.72 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import React, { useState, useImperativeHandle, useRef, useEffect } from 'react'; import { cx } from '@emotion/css'; import { useMemoizedId } from '../utils'; import cvar from '../theme/cvar'; import CloseSvg from '../icons/CloseSvg'; import SuccessSvg from '../icons/SuccessSvg'; import PencilSvg from '../icons/PencilSvg'; import { errorMessage, formControl, inlineEdit, inputBaseStyle, labelStyle, rightAddOn, sizeStyle, successIconWrapper, } from './styles'; import Check from '../icons/Check'; import { InlineButton } from '../internal'; const noop = () => { }; const enterKeyCode = 13; const escKeyCode = 27; const isTextTruthy = (val) => val !== undefined && val !== null && val.toString() !== ''; export const InlineEdit = React.forwardRef((_a, ref) => { var { disabled, forwardedRef, id, label = '', minWidth = 130, name = '', placeholder, required = false, requiredWarningMessage = 'This Field is Required', size = 'default', type = 'text', validateInput = noop, value = '', onBlur = noop, onCancel = noop, onChange, onEditStateChange = noop, onFocus = noop, onSave, className, style } = _a, rest = __rest(_a, ["disabled", "forwardedRef", "id", "label", "minWidth", "name", "placeholder", "required", "requiredWarningMessage", "size", "type", "validateInput", "value", "onBlur", "onCancel", "onChange", "onEditStateChange", "onFocus", "onSave", "className", "style"]); // States for Inline Edit const [localValue, setLocalValue] = useState(value); const [showInput, setShowInput] = useState(false); const [inputWidth, setInputWidth] = useState(); const [savedTimer, setSavedTimer] = useState(); const [contentJustSaved, setContentJustSaved] = useState(false); const [validationError, setValidationError] = useState(); const inputId = useMemoizedId({ id, label }); // ensure we clear any timers in case component unmounts. useEffect(() => () => clearTimeout(savedTimer), [savedTimer]); const previousShowInput = useRef(showInput); useEffect(() => { if (previousShowInput.current !== showInput) { onEditStateChange(showInput); } previousShowInput.current = showInput; }, [showInput, onEditStateChange]); useEffect(() => { setLocalValue(value); }, [value]); useEffect(() => { setValidationError(validateInput(localValue)); }, [localValue, validateInput]); const hasOnChange = Boolean(onChange); const displayValue = hasOnChange ? value : localValue; useEffect(() => { if (!isTextTruthy(displayValue) && required) { setValidationError(requiredWarningMessage); } }, [displayValue, required, requiredWarningMessage]); const inputRef = useRef(); /** * This hook is used to customize the instance value exposed to the parent for the ref. * It must always be used with React.forwardRef. This gives us control of where to pass the ref prop. * Whatever ref is passed to Inline Edit will receive the inputRef instance. * This pattern supports all methods of creating refs. */ useImperativeHandle(forwardedRef, () => inputRef.current); // used to calculate sizing for input to grow with text const valueSizeRef = useRef() || ref; const placeholderSizeRef = useRef(); const errorMessageRef = useRef(); const isTextArea = type === 'textarea'; useEffect(() => { // If there's an onChange handler, the save and cancel buttons are not shown, so the additional // padding is not needed. However, there's still an additional 5px that need to be added. const extraPadding = hasOnChange ? 5 : 70; const pencilPadding = 25; let newWidth = isTextTruthy(displayValue) ? valueSizeRef.current.scrollWidth : placeholderSizeRef.current ? placeholderSizeRef.current.scrollWidth : 0; const errorMessageWidth = errorMessageRef.current ? errorMessageRef.current.scrollWidth : 0; newWidth = Math.max(newWidth, errorMessageWidth); newWidth += showInput ? extraPadding : pencilPadding; if (newWidth < minWidth) { newWidth = minWidth; } setInputWidth(newWidth); }, [displayValue, hasOnChange, valueSizeRef, placeholderSizeRef, minWidth, showInput, validationError]); useEffect(() => { if (showInput && isTextArea) { // Set the cursor to the end of the text and scroll to the end inputRef.current.setSelectionRange(inputRef.current.textContent.length, inputRef.current.textContent.length); inputRef.current.scrollTop = inputRef.current.scrollHeight; } }, [isTextArea, showInput]); const showSuccess = () => { setContentJustSaved(true); setShowInput(false); setSavedTimer(setTimeout(() => { setContentJustSaved(false); }, 1000)); }; const handleBlur = (e) => { if (hasOnChange) { setShowInput(false); showSuccess(); } onBlur(e); }; const handleFocus = (e) => { onFocus(e); }; const handleChange = (e) => { const inputValue = e.target.value; if (hasOnChange) { onChange(e); } else { setLocalValue(inputValue); } }; const handleSaveClick = (e) => { if (!hasOnChange) { if (validationError) { return; } onSave({ value: localValue, name }, e); } showSuccess(); }; // Optional to handle how we cancel our input const handleCancelClick = (e) => { onCancel({ value: localValue, name }, e); setLocalValue(value); setShowInput(false); }; const handleKeyDown = (e) => { e.stopPropagation(); if (e.keyCode === enterKeyCode) { handleSaveClick(e); } else if (e.keyCode === escKeyCode) { handleCancelClick(e); } }; const handleActiveClick = () => { setShowInput(true); }; const inputProps = Object.assign(Object.assign({}, rest), { name, type, value: displayValue, label, placeholder, id: inputId, onKeyDown: handleKeyDown, ref: inputRef, onBlur: handleBlur, onFocus: handleFocus, onChange: handleChange, autoFocus: true }); const inputStyle = Object.assign({ marginBottom: '20px', width: showInput ? `${inputWidth}px` : null }, style); const parentClassName = cx(inlineEdit, { required }, className); const inlineInput = (React.createElement(React.Fragment, null, isTextArea ? (React.createElement("textarea", Object.assign({ className: cx(size, formControl) }, inputProps, { style: { marginTop: '17px', padding: '16px', minWidth: '100px' } }))) : (React.createElement("input", Object.assign({ className: cx(['crc-inline-edit__input', inputBaseStyle(size), size]) }, inputProps))), label ? (React.createElement("label", { className: cx('crc-inline-edit__label', labelStyle), htmlFor: inputId }, label)) : null, !hasOnChange ? (React.createElement("div", { className: cx('crc-inline-edit__right-button', rightAddOn) }, React.createElement("button", { onClick: handleSaveClick, "aria-label": "save", disabled: Boolean(validationError) }, React.createElement(Check, { color: cvar('color-inline-edit-border') })), React.createElement("button", { onClick: handleCancelClick, "aria-label": "cancel" }, React.createElement(CloseSvg, { color: cvar('color-inline-edit-border') })))) : null)); // For disabled state we add the disabled class to the parent // In addition we pass a <p> without the onClick handler const inlineDisplay = (React.createElement(InlineButton, { onClick: disabled ? () => { } : handleActiveClick, className: cx('crc-inline-edit--inactive', { disabled }) }, React.createElement("p", { className: size }, displayValue || placeholder, contentJustSaved && (React.createElement("div", { className: cx('crc-inline-edit__success-icon', successIconWrapper) }, React.createElement(SuccessSvg, { color: cvar('color-background-success') }))), !disabled && (React.createElement("div", { className: "crc-inline-edit__edit-icon" }, React.createElement(PencilSvg, { size: '1em', color: cvar('color-inline-edit-border') })))), label ? React.createElement("div", { className: labelStyle }, label) : null)); return (React.createElement(React.Fragment, null, React.createElement("div", { className: parentClassName, style: inputStyle }, showInput ? inlineInput : inlineDisplay, validationError ? (React.createElement("div", { "data-testid": "error-message", className: cx('crc-inline-edit__error', errorMessage), ref: errorMessageRef }, validationError)) : null), React.createElement("div", { className: size, ref: valueSizeRef, style: sizeStyle }, displayValue), React.createElement("div", { className: size, ref: placeholderSizeRef, style: sizeStyle }, placeholder || ''))); }); InlineEdit.displayName = 'InlineEdit'; //# sourceMappingURL=InlineEdit.js.map