UNPKG

@carbon/ibm-products

Version:
339 lines (337 loc) 11.8 kB
/** * 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;