UNPKG

@carbon/ibm-products

Version:
440 lines (438 loc) 16.3 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_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;