UNPKG

@carbon/ibm-products

Version:

Carbon for IBM Products

555 lines (548 loc) 17.7 kB
/** * Copyright IBM Corp. 2020, 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. */ 'use strict'; var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var React = require('react'); var index = require('../../_virtual/index.js'); var cx = require('classnames'); var react = require('@carbon/react'); var icons = require('@carbon/react/icons'); var APIKeyDownloader = require('./APIKeyDownloader.js'); var settings = require('../../settings.js'); var usePortalTarget = require('../../global/js/hooks/usePortalTarget.js'); var devtools = require('../../global/js/utils/devtools.js'); var propsHelper = require('../../global/js/utils/props-helper.js'); var uuidv4 = require('../../global/js/utils/uuidv4.js'); var useFocus = require('../../global/js/hooks/useFocus.js'); var usePreviousValue = require('../../global/js/hooks/usePreviousValue.js'); var _ErrorFilled, _InformationFilled; const componentName = 'APIKeyModal'; // Default values for props const defaults = { apiKeyName: '', customSteps: [] }; exports.APIKeyModal = /*#__PURE__*/React.forwardRef((_ref, ref) => { let { // The component props, in alphabetical order (for consistency). 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, // Collect any other property values passed in. ...rest } = _ref; const [title, setTitle] = React.useState(null); const [successMessage, setSuccessMessage] = React.useState(null); const [copyError, setCopyError] = React.useState(false); const [name, setName] = React.useState(apiKeyName); const [currentStep, setCurrentStep] = React.useState(0); const copyRef = React.useRef(undefined); const apiKeyInputId = React.useRef(uuidv4.default()); const nameInputId = React.useRef(uuidv4.default()); const renderPortalUse = 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.createElement(icons.Copy, _rollupPluginBabelHelpers.extends({ size: 16 }, props)), iconDescription: copyIconDescription, ref: copyRef }; const blockClass = `${settings.pkg.prefix}--apikey-modal`; const localRef = React.useRef(undefined); const PasswordInputRef = React.useRef(null); const modalRef = ref || localRef; const { firstElement, keyDownListener, claimFocus } = useFocus.useFocus(modalRef); const prevOpen = usePreviousValue.usePreviousValue(open); React.useEffect(() => { if (copyRef.current && open && apiKeyLoaded) { copyRef.current.focus(); } if (PasswordInputRef?.current) { PasswordInputRef?.current.setAttribute('readOnly', 'true'); } }, [open, apiKeyLoaded]); React.useEffect(() => { if (open) { // Focusing the first element or selectorPrimaryFocus element claimFocus(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [modalRef, open, firstElement]); 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; }; 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.createElement(react.unstable_FeatureFlags, { enableExperimentalFocusWrapWithoutSentinels: true }, /*#__PURE__*/React.createElement(react.ComposedModal, _rollupPluginBabelHelpers.extends({}, rest, { open }, devtools.getDevtoolsProps(componentName), { ref: modalRef, onKeyDown: keyDownListener, className: cx(className, blockClass), onClose: onCloseHandler, size: "sm", "aria-label": modalLabel, preventCloseOnClickOutside: true }), /*#__PURE__*/React.createElement(react.ModalHeader, { className: `${blockClass}__header`, title: title, label: modalLabel }), /*#__PURE__*/React.createElement(react.ModalBody, { className: `${blockClass}__body-container` }, hasSteps && !apiKeyLoaded ? customSteps[currentStep]?.content : /*#__PURE__*/React.createElement(React.Fragment, null, body && /*#__PURE__*/React.createElement("p", { className: `${blockClass}__body` }, body), !editing && apiKey && hasAPIKeyVisibilityToggle && /*#__PURE__*/React.createElement(react.PasswordInput, { value: apiKey, labelText: apiKeyLabel, id: apiKeyInputId.current, showPasswordLabel: showAPIKeyLabel, hidePasswordLabel: hideAPIKeyLabel, tooltipPosition: "left", helperText: helperText, ref: PasswordInputRef }), !editing && apiKey && !hasAPIKeyVisibilityToggle && /*#__PURE__*/React.createElement(react.TextInput, { value: apiKey, labelText: apiKeyLabel, id: apiKeyInputId.current }), (editing || !apiKeyLoaded && nameRequired) && /*#__PURE__*/React.createElement(react.Form, { onSubmit: submitHandler, "aria-label": title ?? undefined }, /*#__PURE__*/React.createElement(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.createElement(react.InlineLoading, { description: loadingText, className: `${blockClass}__loader` }), (copyError || error) && /*#__PURE__*/React.createElement("div", { className: `${blockClass}__messaging` }, /*#__PURE__*/React.createElement("div", { className: `${blockClass}__error-icon` }, _ErrorFilled || (_ErrorFilled = /*#__PURE__*/React.createElement(icons.ErrorFilled, { size: 16 }))), /*#__PURE__*/React.createElement("p", { className: `${blockClass}__messaging-text`, role: "alert", "aria-live": "assertive" }, copyError ? copyErrorText : errorText)), apiKeyLoaded && /*#__PURE__*/React.createElement("div", { className: `${blockClass}__messaging` }, _InformationFilled || (_InformationFilled = /*#__PURE__*/React.createElement(icons.InformationFilled, { size: 16 })), hasDownloadLink ? /*#__PURE__*/React.createElement(APIKeyDownloader.APIKeyDownloader, { apiKey: apiKey, body: downloadBodyText, fileName: downloadFileName, linkText: downloadLinkText, fileType: downloadFileType, downloadLinkLabel: downloadLinkLabel }) : /*#__PURE__*/React.createElement("div", { className: `${blockClass}__messaging-text`, role: "alert", "aria-live": "assertive" }, generateSuccessBody)), (editSuccess || apiKeyLoaded && successMessage) && /*#__PURE__*/React.createElement("div", { className: `${blockClass}__messaging` }, /*#__PURE__*/React.createElement(icons.CheckmarkFilled, { size: 16, className: `${blockClass}__checkmark-icon` }), /*#__PURE__*/React.createElement("p", { className: `${blockClass}__messaging-text`, role: "alert", "aria-live": "assertive" }, successMessage)))), /*#__PURE__*/React.createElement(react.ModalFooter, { className: `${blockClass}__footer` }, /*#__PURE__*/React.createElement(react.Button, { type: "button", kind: "secondary", onClick: onBackHandler }, getSecondaryButtonText()), /*#__PURE__*/React.createElement(react.Button, _rollupPluginBabelHelpers.extends({}, apiKeyLoaded && copyButtonProps, { type: "submit", kind: "primary", onClick: submitHandler, disabled: isPrimaryButtonDisabled() }), getPrimaryButtonText()))))); }); const customStepsRequiredProps = type => propsHelper.isRequiredIf(type, _ref2 => { let { customSteps } = _ref2; return customSteps && customSteps.length > 1; }); const editRequiredProps = type => propsHelper.isRequiredIf(type, _ref3 => { let { editing } = _ref3; return editing; }); const downloadRequiredProps = type => propsHelper.isRequiredIf(type, _ref4 => { let { hasDownloadLink } = _ref4; return hasDownloadLink; }); // Return a placeholder if not released and not enabled by feature flag exports.APIKeyModal = settings.pkg.checkComponentEnabled(exports.APIKeyModal, componentName); const deprecatedProps = { /** * deprecated * title for a successful edit */ editSuccessTitle: index.default.string, /** * deprecated * title for a successful key generation */ generateSuccessTitle: index.default.string }; exports.APIKeyModal.propTypes = { /** * the api key that's displayed to the user when a request to create is fulfilled. */ apiKey: index.default.string, /** * label for the text input that holds the api key. */ apiKeyLabel: index.default.string, /** * the name of the api key. should only be supplied in edit mode. */ apiKeyName: index.default.string, /** * body content for the modal */ body: index.default.string, /** * optional class name */ className: index.default.string, /** * text for the close button */ closeButtonText: index.default.string, /** * text for the copy button */ copyButtonText: index.default.string, /** * Error message for when the copy function fails */ copyErrorText: index.default.string, /** * text description for the copy button icon */ copyIconDescription: index.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: index.default.arrayOf(index.default.shape({ /** * designates if the step has passed whatever validation rules are in place. */ valid: index.default.bool, /** * designates content is the JSX that holds whatever inputs you need */ content: index.default.node, /** * designates the title that's displayed at the top of the modal for each step */ title: index.default.string })), /** * the content that appears that indicates the key is downloadable */ downloadBodyText: index.default.string, /** * designates the name of downloadable json file with the key. if not specified will default to 'apikey' */ downloadFileName: downloadRequiredProps(index.default.string), /** * designates the file type for the downloadable key */ downloadFileType: downloadRequiredProps(index.default.oneOf(['txt', 'json'])), /** * aria-label for the download link */ downloadLinkLabel: downloadRequiredProps(index.default.string), /** * anchor text for the download link */ downloadLinkText: downloadRequiredProps(index.default.string), /** * text for the edit button */ editButtonText: editRequiredProps(index.default.string), /** * designates if the edit request was successful */ editSuccess: editRequiredProps(index.default.bool), /** * title for a successful edit */ editSuccessMessage: editRequiredProps(index.default.string), /** * designates if the modal is in the edit mode */ editing: index.default.bool, /** * designates if an error has occurred in a request */ error: index.default.bool, /** * text to display if an error has occurred */ errorText: index.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: index.default.string, /** * content to display if generate request was successful */ generateSuccessBody: index.default.node, /** * title for a successful key generation */ generateSuccessMessage: index.default.string, /** * default title for the modal in generate key mode */ generateTitle: index.default.string, /** * designates if the api input has the visibility toggle enabled */ hasAPIKeyVisibilityToggle: index.default.bool, /** * designates if user is able to download the api key */ hasDownloadLink: index.default.bool, /** * helper text for password input */ helperText: index.default.string, /** * label text that's displayed when hovering over visibility toggler to hide key */ hideAPIKeyLabel: index.default.string, /** * Provide a ref to return focus to once the tearsheet is closed. */ /**@ts-ignore */ launcherButtonRef: index.default.any, /** * designates if the modal is in a loading state via a request or some other in progress operation */ loading: index.default.bool, /** * text that displays while modal is in the loading state */ loadingText: index.default.string, /** * general label text for modal */ modalLabel: index.default.string, /** * helper text for name input */ nameHelperText: index.default.string, /** * label for api key name input */ nameLabel: index.default.string, /** * placeholder text for api key name input */ namePlaceholder: index.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: index.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: customStepsRequiredProps(index.default.string), /** * handler for on modal close */ onClose: index.default.func, /** * Optional callback if you want to use your own copy function instead of the build in one * onCopy(apiKey) */ onCopy: index.default.func, /** * handler for api key edit */ onRequestEdit: index.default.func, /** * handler for api key generation */ onRequestGenerate: index.default.func, /** * designates if modal is open or closed */ open: index.default.bool.isRequired, /** * The DOM node the tearsheet should be rendered within. Defaults to document.body. */ portalTarget: index.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: customStepsRequiredProps(index.default.string), /** * label text that's displayed when hovering over visibility toggler to show key */ showAPIKeyLabel: index.default.string, ...deprecatedProps }; exports.APIKeyModal.displayName = componentName; exports.deprecatedProps = deprecatedProps;