@cimpress/react-components
Version:
React components to support the MCP styleguide
180 lines • 9.72 kB
JavaScript
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