@carbon/ibm-products
Version:
Carbon for IBM Products
339 lines (337 loc) • 11.8 kB
JavaScript
/**
* Copyright IBM Corp. 2020, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const require_runtime = require("../../_virtual/_rolldown/runtime.js");
const require_index = require("../../node_modules/classnames/index.js");
const require_settings = require("../../settings.js");
const require_devtools = require("../../global/js/utils/devtools.js");
let react = require("react");
react = require_runtime.__toESM(react);
let prop_types = require("prop-types");
prop_types = require_runtime.__toESM(prop_types);
let _carbon_react = require("@carbon/react");
let _carbon_react_icons = require("@carbon/react/icons");
//#region src/components/EditInPlace/EditInPlace.tsx
/**
* Copyright IBM Corp. 2022, 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
var import_classnames = /* @__PURE__ */ require_runtime.__toESM(require_index.default);
const componentName = "EditInPlace";
const blockClass = `${require_settings.pkg.prefix}--edit-in-place`;
const defaults = {
tooltipAlignment: "top",
size: "sm"
};
const EditInPlace = (0, react.forwardRef)(({ cancelLabel, editAlwaysVisible, editLabel, id, inheritTypography, invalid, invalidLabel: deprecated_invalidLabel = " ", invalidText, labelText, onCancel, onChange, onSave, onBlur, readOnly, readOnlyLabel, readOnlyToggleTipText, saveLabel, size = "sm", toggleTipAlignment, tooltipAlignment, value, placeholder, ...rest }, ref) => {
const [focused, setFocused] = (0, react.useState)(false);
const [initialValue, setInitialValue] = (0, react.useState)(value);
const [dirtyInput, setDirtyInput] = (0, react.useState)(false);
const inputRef = (0, react.useRef)(null);
const hasValueChanged = value.trim() !== initialValue.trim();
const canSave = hasValueChanged && !invalid;
const canCancel = hasValueChanged;
const escaping = (0, react.useRef)(false);
const clickingWithin = (0, react.useRef)(false);
const carbonPrefix = (0, _carbon_react.usePrefix)();
const tipAlignIsObject = typeof tooltipAlignment === "object";
const tipAlignments = [
"edit",
"save",
"cancel"
].reduce((acc, tips) => {
acc[tips] = (tipAlignIsObject ? tooltipAlignment[tips] : tooltipAlignment) ?? defaults.tooltipAlignment;
return acc;
}, {});
const isTargetingChild = ({ currentTarget, relatedTarget }) => currentTarget.contains(relatedTarget);
const onChangeHandler = ({ target }) => {
if (!dirtyInput) setDirtyInput(true);
onChange(target.value);
};
const onFocusHandler = (e) => {
if (!isTargetingChild(e)) inputRef.current?.focus();
setFocused(true);
};
const onSaveHandler = (exitEditMode = false) => {
setInitialValue(value);
setDirtyInput(false);
onSave();
if (exitEditMode) setFocused(false);
else requestAnimationFrame(() => {
inputRef.current?.focus();
});
};
const onCancelHandler = (exitEditMode = false) => {
setDirtyInput(false);
onCancel(initialValue);
if (exitEditMode) setFocused(false);
else requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus();
const length = inputRef.current.value.length;
inputRef.current.setSelectionRange(length, length);
}
});
};
const onBlurHandler = (e) => {
const clickedWithin = clickingWithin.current;
const targetingChild = isTargetingChild(e);
if (clickedWithin && targetingChild) {
clickingWithin.current = false;
return;
}
if (clickedWithin && !targetingChild) clickingWithin.current = false;
if (!clickedWithin && targetingChild) return;
if (typeof onBlur === "function") {
onBlur(initialValue);
setFocused(false);
} else {
if (escaping.current) return;
if (canSave) onSaveHandler(true);
else {
onCancelHandler(true);
setFocused(false);
}
}
};
const returnHandler = () => {
if (canSave) onSaveHandler();
};
const escapeHandler = () => {
onCancelHandler();
};
const removeFocus = () => {
inputRef.current?.blur();
setFocused(false);
};
const handleToolbarMouseDown = (e) => {
const element = e.target;
let foundButton = null;
let current = element;
while (current && current !== e.currentTarget) {
if (current.tagName === "BUTTON") {
foundButton = current;
break;
}
current = current.parentElement;
}
if (!foundButton) foundButton = element.querySelector("button");
if (foundButton) {
clickingWithin.current = true;
if (foundButton.hasAttribute("disabled") || foundButton.getAttribute("aria-disabled") === "true") e.preventDefault();
}
};
const onKeyHandler = (e) => {
escaping.current = true;
switch (e.key) {
case "Escape":
removeFocus();
escapeHandler();
break;
case "Enter":
removeFocus();
returnHandler();
break;
default: break;
}
escaping.current = false;
};
const inputElement = /* @__PURE__ */ react.default.createElement("input", {
id,
className: (0, import_classnames.default)(`${blockClass}__text-input`, `${carbonPrefix}--text-input`, `${carbonPrefix}--text-input--${size}`),
type: "text",
placeholder,
value,
onChange: onChangeHandler,
ref: inputRef,
readOnly,
onKeyDown: onKeyHandler,
"aria-label": labelText,
"aria-invalid": invalid
});
const inputContainer = /* @__PURE__ */ react.default.createElement("div", {
className: (0, import_classnames.default)(blockClass, `${blockClass}--${size}`, {
[`${blockClass}--focused`]: focused,
[`${blockClass}--invalid`]: invalid,
[`${blockClass}--inherit-type`]: inheritTypography,
[`${blockClass}--overflows`]: inputRef.current && inputRef.current.scrollWidth > inputRef.current.offsetWidth,
[`${blockClass}--readonly`]: readOnly
}),
onFocus: onFocusHandler,
onBlur: onBlurHandler
}, readOnly ? /* @__PURE__ */ react.default.createElement(_carbon_react.Toggletip, {
align: toggleTipAlignment,
className: `${blockClass}__toggletip-wrapper`
}, /* @__PURE__ */ react.default.createElement(_carbon_react.ToggletipButton, { label: readOnlyLabel || "Edit off" }, inputElement), /* @__PURE__ */ react.default.createElement(_carbon_react.ToggletipContent, null, /* @__PURE__ */ react.default.createElement("p", null, readOnlyToggleTipText || "This field is read-only and cannot be edited"))) : inputElement, /* @__PURE__ */ react.default.createElement("div", {
className: `${blockClass}__toolbar`,
onMouseDown: handleToolbarMouseDown
}, invalid && /* @__PURE__ */ react.default.createElement(_carbon_react_icons.WarningFilled, {
size: 16,
className: `${blockClass}__warning-icon`
}), readOnly ? /* @__PURE__ */ react.default.createElement(_carbon_react.IconButton, {
className: `${blockClass}__btn-readonly`,
size,
label: readOnlyLabel || "Edit off",
kind: "ghost",
key: "readonly",
onClick: onFocusHandler
}, /* @__PURE__ */ react.default.createElement(_carbon_react_icons.EditOff, { size: 16 })) : focused ? /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, /* @__PURE__ */ react.default.createElement(_carbon_react.IconButton, {
align: tipAlignments.cancel,
size,
label: cancelLabel,
onClick: () => onCancelHandler(false),
kind: "ghost",
key: "cancel",
className: `${blockClass}__btn ${blockClass}__btn-cancel`,
disabled: !canCancel
}, /* @__PURE__ */ react.default.createElement(_carbon_react_icons.Close, { size: 16 })), /* @__PURE__ */ react.default.createElement(_carbon_react.IconButton, {
align: tipAlignments.save,
size,
label: saveLabel,
onClick: () => onSaveHandler(false),
kind: "ghost",
key: "save",
className: `${blockClass}__btn ${blockClass}__btn-save`,
disabled: !canSave
}, /* @__PURE__ */ react.default.createElement(_carbon_react_icons.Checkmark, { size: 16 }))) : /* @__PURE__ */ react.default.createElement(_carbon_react.IconButton, {
align: tipAlignments.edit,
className: (0, import_classnames.default)(`${blockClass}__btn`, `${blockClass}__btn-edit`, { [`${blockClass}__btn-edit--always-visible`]: editAlwaysVisible }),
size,
label: editLabel,
onClick: onFocusHandler,
kind: "ghost",
key: "edit"
}, /* @__PURE__ */ react.default.createElement(_carbon_react_icons.Edit, { size: 16 }))));
return /* @__PURE__ */ react.default.createElement("div", {
...rest,
ref,
...require_devtools.getDevtoolsProps(componentName)
}, inputContainer, invalid && /* @__PURE__ */ react.default.createElement("p", { className: `${blockClass}__warning-text` }, invalidText ?? deprecated_invalidLabel));
});
EditInPlace.displayName = componentName;
const deprecatedProps = {
/**
* **Deprecated**
* invalidLabel was misnamed, using invalidText to match Carbon
*/
invalidText: prop_types.default.string };
const alignPropType = prop_types.default.oneOf([
"top",
"top-left",
"top-right",
"bottom",
"bottom-left",
"bottom-right",
"left",
"right"
]);
EditInPlace.propTypes = {
/**
* label for cancel button
*/
cancelLabel: prop_types.default.string.isRequired,
/**
* By default the edit icon is shown on hover only.
*/
editAlwaysVisible: prop_types.default.bool,
/**
* label for edit button
*/
editLabel: prop_types.default.string.isRequired,
/**
* Specify a custom id for the input
*/
id: prop_types.default.string.isRequired,
/**
* inheritTypography - causes the text entry field to inherit typography settings
* assigned to the container. This is useful when editing titles for instance.
*
* NOTE: The size property limits the vertical size of the input element.
* Inherited font's should be selected to fit within the size selected.
*/
inheritTypography: prop_types.default.bool,
/**
* determines if the input is invalid
*/
invalid: prop_types.default.bool,
/**
* text that is displayed if the input is invalid
*/
/**@ts-ignore*/
invalidText: prop_types.default.string,
/**
* Provide the text that will be read by a screen reader when visiting this control
*/
labelText: prop_types.default.string.isRequired,
/**
* handler to add custom onBlur event
*/
onBlur: prop_types.default.func,
/**
* handler that is called when the cancel button is pressed or when the user removes focus from the input and there is no new value
*/
onCancel: prop_types.default.func.isRequired,
/**
* handler that is called when the input is updated
*/
onChange: prop_types.default.func.isRequired,
/**
* handler that is called when the save button is pressed or when the user removes focus from the input if it has a new value
*/
onSave: prop_types.default.func.isRequired,
/**
* Placeholder for text input
*/
placeholder: prop_types.default.string,
/**
* determines if the input is in readOnly mode
*/
readOnly: prop_types.default.bool,
/**
* label for the edit off button that displays when in read only mode
*/
readOnlyLabel: prop_types.default.string,
/**
* text for the toggletip that displays when in read only mode
*/
readOnlyToggleTipText: prop_types.default.string,
/**
* label for save button
*/
saveLabel: prop_types.default.string.isRequired,
/**
* vertical size of control
*/
size: prop_types.default.oneOf([
"sm",
"md",
"lg"
]),
/**
* alignment for the toggletip that displays when in read only mode
*/
toggleTipAlignment: alignPropType,
/**
* tooltipAlignment from the standard tooltip. Default center.
*
* Can be passed either as one of tooltip options or as an object specifying cancel, edit and save separately
*/
/**@ts-ignore*/
tooltipAlignment: prop_types.default.oneOfType([alignPropType, prop_types.default.shape({
cancel: alignPropType,
edit: alignPropType,
save: alignPropType
})]),
/**
* current value of the input
*/
value: prop_types.default.string.isRequired,
...deprecatedProps
};
//#endregion
exports.EditInPlace = EditInPlace;