@carbon/ibm-products
Version:
Carbon for IBM Products
440 lines (438 loc) • 16.3 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_usePreviousValue = require("../../global/js/hooks/usePreviousValue.js");
const require_useFocus = require("../../global/js/hooks/useFocus.js");
const require_devtools = require("../../global/js/utils/devtools.js");
const require_usePortalTarget = require("../../global/js/hooks/usePortalTarget.js");
const require_uuidv4 = require("../../global/js/utils/uuidv4.js");
const require_APIKeyDownloader = require("./APIKeyDownloader.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/APIKeyModal/APIKeyModal.tsx
var import_classnames = /* @__PURE__ */ require_runtime.__toESM(require_index.default);
const componentName = "APIKeyModal";
const defaults = {
apiKeyName: "",
customSteps: []
};
const APIKeyModal = (0, react.forwardRef)(({ apiKey, apiKeyLabel, apiKeyName = defaults.apiKeyName, body, className, closeButtonText, copyButtonText, copyErrorText, copyIconDescription, customSteps = defaults.customSteps, downloadBodyText, downloadFileName, downloadFileType, downloadLinkText, downloadLinkLabel = downloadLinkText, editButtonText, editSuccess, editSuccessTitle, editSuccessMessage, editing, error, errorText, generateButtonText, generateSuccessBody, generateSuccessTitle, generateSuccessMessage, generateTitle, hasAPIKeyVisibilityToggle, hasDownloadLink, hideAPIKeyLabel, launcherButtonRef, loading, loadingText, modalLabel, nameHelperText, nameLabel, namePlaceholder, nameRequired, nextStepButtonText, onClose, onCopy, onRequestEdit, onRequestGenerate, open, portalTarget: portalTargetIn, previousStepButtonText, showAPIKeyLabel, helperText, ...rest }, ref) => {
const [title, setTitle] = (0, react.useState)(null);
const [successMessage, setSuccessMessage] = (0, react.useState)(null);
const [copyError, setCopyError] = (0, react.useState)(false);
const [name, setName] = (0, react.useState)(apiKeyName);
const [currentStep, setCurrentStep] = (0, react.useState)(0);
const copyRef = (0, react.useRef)(void 0);
const apiKeyInputId = (0, react.useRef)(require_uuidv4.default());
const nameInputId = (0, react.useRef)(require_uuidv4.default());
const renderPortalUse = require_usePortalTarget.usePortalTarget(portalTargetIn);
const hasSteps = Boolean(customSteps.length);
const apiKeyLoaded = apiKey && !loading;
const hasNextStep = hasSteps && currentStep < customSteps.length - 1;
const hasPreviousStep = hasSteps && currentStep !== 0;
const copyButtonProps = {
renderIcon: (props) => /* @__PURE__ */ react.default.createElement(_carbon_react_icons.Copy, {
size: 16,
...props
}),
iconDescription: copyIconDescription,
ref: copyRef
};
const blockClass = `${require_settings.pkg.prefix}--apikey-modal`;
const localRef = (0, react.useRef)(void 0);
const PasswordInputRef = (0, react.useRef)(null);
const modalRef = ref || localRef;
const { firstElement, keyDownListener, claimFocus } = require_useFocus.useFocus(modalRef);
const prevOpen = require_usePreviousValue.usePreviousValue(open);
(0, react.useEffect)(() => {
if (copyRef.current && open && apiKeyLoaded) copyRef.current.focus();
if (PasswordInputRef?.current) PasswordInputRef?.current.setAttribute("readOnly", "true");
}, [open, apiKeyLoaded]);
(0, react.useEffect)(() => {
if (open) setTimeout(() => claimFocus(), 0);
}, [
modalRef,
open,
firstElement
]);
(0, react.useEffect)(() => {
if (prevOpen && !open && launcherButtonRef) setTimeout(() => {
launcherButtonRef.current.focus();
}, 0);
}, [
launcherButtonRef,
open,
prevOpen
]);
const isPrimaryButtonDisabled = () => {
if (loading) return true;
if (hasSteps && "valid" in (customSteps?.[currentStep] || [])) return !customSteps[currentStep]?.valid;
if (!hasSteps && nameRequired && !name) return true;
return false;
};
const getPrimaryButtonText = () => {
if (hasNextStep) return nextStepButtonText;
if (apiKeyLoaded) return copyButtonText;
if (editing) return editButtonText;
return generateButtonText;
};
const getSecondaryButtonText = () => {
if (hasPreviousStep && !apiKeyLoaded) return previousStepButtonText;
return closeButtonText;
};
(0, react.useEffect)(() => {
if (editing && editSuccess) {
setTitle(generateTitle);
setSuccessMessage(editSuccessMessage ?? editSuccessTitle);
} else if (apiKeyLoaded) {
setTitle(generateTitle);
setSuccessMessage(generateSuccessMessage ?? generateSuccessTitle);
} else if (hasSteps) setTitle(customSteps[currentStep]?.title);
else setTitle(generateTitle);
}, [
apiKeyLoaded,
loading,
editing,
editSuccess,
editSuccessTitle,
editSuccessMessage,
hasSteps,
generateSuccessTitle,
generateSuccessMessage,
generateTitle,
currentStep,
customSteps
]);
const setNameHandler = (evt) => {
setName(evt.target.value);
};
const onCloseHandler = () => {
setName("");
setCurrentStep(0);
onClose?.();
};
const submitHandler = async (e) => {
e.preventDefault();
if (hasNextStep) setCurrentStep(currentStep + 1);
else if (apiKeyLoaded) if (onCopy) onCopy(apiKey);
else try {
await navigator.clipboard.writeText(apiKey);
} catch (e) {
console.error(e);
setCopyError(true);
}
else if (editing) onRequestEdit?.(name);
else onRequestGenerate?.(name);
};
const onBackHandler = () => {
if (hasPreviousStep && !apiKeyLoaded) setCurrentStep(currentStep - 1);
else onCloseHandler();
};
return renderPortalUse(/* @__PURE__ */ react.default.createElement(_carbon_react.unstable_FeatureFlags, { enableExperimentalFocusWrapWithoutSentinels: true }, /* @__PURE__ */ react.default.createElement(_carbon_react.ComposedModal, {
...rest,
open,
...require_devtools.getDevtoolsProps(componentName),
ref: modalRef,
onKeyDown: keyDownListener,
className: (0, import_classnames.default)(className, blockClass),
onClose: onCloseHandler,
size: "sm",
"aria-label": modalLabel,
preventCloseOnClickOutside: true
}, /* @__PURE__ */ react.default.createElement(_carbon_react.ModalHeader, {
className: `${blockClass}__header`,
title,
label: modalLabel
}), /* @__PURE__ */ react.default.createElement(_carbon_react.ModalBody, { className: `${blockClass}__body-container` }, hasSteps && !apiKeyLoaded ? customSteps[currentStep]?.content : /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, body && /* @__PURE__ */ react.default.createElement("p", { className: `${blockClass}__body` }, body), !editing && apiKey && hasAPIKeyVisibilityToggle && /* @__PURE__ */ react.default.createElement(_carbon_react.PasswordInput, {
value: apiKey,
labelText: apiKeyLabel,
id: apiKeyInputId.current,
showPasswordLabel: showAPIKeyLabel,
hidePasswordLabel: hideAPIKeyLabel,
tooltipPosition: "left",
helperText,
ref: PasswordInputRef
}), !editing && apiKey && !hasAPIKeyVisibilityToggle && /* @__PURE__ */ react.default.createElement(_carbon_react.TextInput, {
value: apiKey,
labelText: apiKeyLabel,
id: apiKeyInputId.current
}), (editing || !apiKeyLoaded && nameRequired) && /* @__PURE__ */ react.default.createElement(_carbon_react.Form, {
onSubmit: submitHandler,
"aria-label": title ?? void 0
}, /* @__PURE__ */ react.default.createElement(_carbon_react.TextInput, {
helperText: nameHelperText,
placeholder: namePlaceholder,
labelText: nameLabel,
onChange: setNameHandler,
value: name,
id: nameInputId.current,
disabled: loading,
required: nameRequired,
"data-modal-primary-focus": true
})), loading && /* @__PURE__ */ react.default.createElement(_carbon_react.InlineLoading, {
description: loadingText,
className: `${blockClass}__loader`
}), (copyError || error) && /* @__PURE__ */ react.default.createElement("div", { className: `${blockClass}__messaging` }, /* @__PURE__ */ react.default.createElement("div", { className: `${blockClass}__error-icon` }, /* @__PURE__ */ react.default.createElement(_carbon_react_icons.ErrorFilled, { size: 16 })), /* @__PURE__ */ react.default.createElement("p", {
className: `${blockClass}__messaging-text`,
role: "alert",
"aria-live": "assertive"
}, copyError ? copyErrorText : errorText)), apiKeyLoaded && /* @__PURE__ */ react.default.createElement("div", { className: `${blockClass}__messaging` }, /* @__PURE__ */ react.default.createElement(_carbon_react_icons.InformationFilled, { size: 16 }), hasDownloadLink ? /* @__PURE__ */ react.default.createElement(require_APIKeyDownloader.APIKeyDownloader, {
apiKey,
body: downloadBodyText,
fileName: downloadFileName,
linkText: downloadLinkText,
fileType: downloadFileType,
downloadLinkLabel
}) : /* @__PURE__ */ react.default.createElement("div", {
className: `${blockClass}__messaging-text`,
role: "alert",
"aria-live": "assertive"
}, generateSuccessBody)), (editSuccess || apiKeyLoaded && successMessage) && /* @__PURE__ */ react.default.createElement("div", { className: `${blockClass}__messaging` }, /* @__PURE__ */ react.default.createElement(_carbon_react_icons.CheckmarkFilled, {
size: 16,
className: `${blockClass}__checkmark-icon`
}), /* @__PURE__ */ react.default.createElement("p", {
className: `${blockClass}__messaging-text`,
role: "alert",
"aria-live": "assertive"
}, successMessage)))), /* @__PURE__ */ react.default.createElement(_carbon_react.ModalFooter, { className: `${blockClass}__footer` }, /* @__PURE__ */ react.default.createElement(_carbon_react.Button, {
type: "button",
kind: "secondary",
onClick: onBackHandler
}, getSecondaryButtonText()), /* @__PURE__ */ react.default.createElement(_carbon_react.Button, {
...apiKeyLoaded && copyButtonProps,
type: "submit",
kind: "primary",
onClick: submitHandler,
disabled: isPrimaryButtonDisabled()
}, getPrimaryButtonText())))));
});
const deprecatedProps = {
/**
* deprecated
* title for a successful edit
*/
editSuccessTitle: prop_types.default.string,
/**
* deprecated
* title for a successful key generation
*/
generateSuccessTitle: prop_types.default.string
};
APIKeyModal.propTypes = {
/**
* the api key that's displayed to the user when a request to create is fulfilled.
*/
apiKey: prop_types.default.string,
/**
* label for the text input that holds the api key.
*/
apiKeyLabel: prop_types.default.string,
/**
* the name of the api key. should only be supplied in edit mode.
*/
apiKeyName: prop_types.default.string,
/**
* body content for the modal
*/
body: prop_types.default.string,
/**
* optional class name
*/
className: prop_types.default.string,
/**
* text for the close button
*/
closeButtonText: prop_types.default.string,
/**
* text for the copy button
*/
copyButtonText: prop_types.default.string,
/**
* Error message for when the copy function fails
*/
copyErrorText: prop_types.default.string,
/**
* text description for the copy button icon
*/
copyIconDescription: prop_types.default.string,
/**
* if you need more options for key creation beyond just the name use custom steps to obtain whatever data is required.
*/
/**@ts-ignore*/
customSteps: prop_types.default.arrayOf(prop_types.default.shape({
/**
* designates if the step has passed whatever validation rules are in place.
*/
valid: prop_types.default.bool,
/**
* designates content is the JSX that holds whatever inputs you need
*/
content: prop_types.default.node,
/**
* designates the title that's displayed at the top of the modal for each step
*/
title: prop_types.default.string
})),
/**
* the content that appears that indicates the key is downloadable
*/
downloadBodyText: prop_types.default.string,
/**
* designates the name of downloadable json file with the key. if not specified will default to 'apikey'
*/
downloadFileName: prop_types.default.string,
/**
* designates the file type for the downloadable key
*/
downloadFileType: prop_types.default.oneOf(["txt", "json"]),
/**
* aria-label for the download link
*/
downloadLinkLabel: prop_types.default.string,
/**
* anchor text for the download link
*/
downloadLinkText: prop_types.default.string,
/**
* text for the edit button
*/
editButtonText: prop_types.default.string,
/**
* designates if the edit request was successful
*/
editSuccess: prop_types.default.bool,
/**
* title for a successful edit
*/
editSuccessMessage: prop_types.default.string,
/**
* designates if the modal is in the edit mode
*/
editing: prop_types.default.bool,
/**
* designates if an error has occurred in a request
*/
error: prop_types.default.bool,
/**
* text to display if an error has occurred
*/
errorText: prop_types.default.string,
/**
* default primary button text for modal in assumed default mode create or generate.
* in create mode this is the button text prior to supplying an api key, which then
* uses copyButtonText
*/
generateButtonText: prop_types.default.string,
/**
* content to display if generate request was successful
*/
generateSuccessBody: prop_types.default.node,
/**
* title for a successful key generation
*/
generateSuccessMessage: prop_types.default.string,
/**
* default title for the modal in generate key mode
*/
generateTitle: prop_types.default.string,
/**
* designates if the api input has the visibility toggle enabled
*/
hasAPIKeyVisibilityToggle: prop_types.default.bool,
/**
* designates if user is able to download the api key
*/
hasDownloadLink: prop_types.default.bool,
/**
* helper text for password input
*/
helperText: prop_types.default.string,
/**
* label text that's displayed when hovering over visibility toggler to hide key
*/
hideAPIKeyLabel: prop_types.default.string,
/**
* Provide a ref to return focus to once the tearsheet is closed.
*/
/**@ts-ignore */
launcherButtonRef: prop_types.default.any,
/**
* designates if the modal is in a loading state via a request or some other in progress operation
*/
loading: prop_types.default.bool,
/**
* text that displays while modal is in the loading state
*/
loadingText: prop_types.default.string,
/**
* general label text for modal
*/
modalLabel: prop_types.default.string,
/**
* helper text for name input
*/
nameHelperText: prop_types.default.string,
/**
* label for api key name input
*/
nameLabel: prop_types.default.string,
/**
* placeholder text for api key name input
*/
namePlaceholder: prop_types.default.string,
/**
* designates if a name is required or not for key generation. NOTE- if using custom steps set this to false since you will be using your own validation
*/
nameRequired: prop_types.default.bool,
/**
* text that displays in the primary button when using custom steps to indicate to the user that there is a next step
*/
nextStepButtonText: prop_types.default.string,
/**
* handler for on modal close
*/
onClose: prop_types.default.func,
/**
* Optional callback if you want to use your own copy function instead of the build in one
* onCopy(apiKey)
*/
onCopy: prop_types.default.func,
/**
* handler for api key edit
*/
onRequestEdit: prop_types.default.func,
/**
* handler for api key generation
*/
onRequestGenerate: prop_types.default.func,
/**
* designates if modal is open or closed
*/
open: prop_types.default.bool.isRequired,
/**
* The DOM node the tearsheet should be rendered within. Defaults to document.body.
*/
portalTarget: prop_types.default.node,
/**
* text that displays in the secondary button when using custom steps to indicate to the user that there is a previous step
*/
previousStepButtonText: prop_types.default.string,
/**
* label text that's displayed when hovering over visibility toggler to show key
*/
showAPIKeyLabel: prop_types.default.string,
...deprecatedProps
};
APIKeyModal.displayName = componentName;
//#endregion
exports.APIKeyModal = APIKeyModal;