diff --git a/packages/ibm-products-styles/src/__tests__/__snapshots__/styles.test.js.snap b/packages/ibm-products-styles/src/__tests__/__snapshots__/styles.test.js.snap
index 58081841e0..da590c0057 100644
--- a/packages/ibm-products-styles/src/__tests__/__snapshots__/styles.test.js.snap
+++ b/packages/ibm-products-styles/src/__tests__/__snapshots__/styles.test.js.snap
@@ -129,6 +129,10 @@ p.c4p--about-modal__copyright-text:first-child {
fill: var(--cds-button-danger-primary, #da1e28);
}
+.c4p--apikey-modal__checkmark-icon {
+ fill: var(--cds-button-primary, #0f62fe);
+}
+
.c4p--action-set {
align-items: stretch;
justify-content: flex-end;
diff --git a/packages/ibm-products-styles/src/components/APIKeyModal/_api-key-modal.scss b/packages/ibm-products-styles/src/components/APIKeyModal/_api-key-modal.scss
index f5b19278a5..8b0cf5202d 100644
--- a/packages/ibm-products-styles/src/components/APIKeyModal/_api-key-modal.scss
+++ b/packages/ibm-products-styles/src/components/APIKeyModal/_api-key-modal.scss
@@ -50,3 +50,7 @@ $block-class: #{c4p-settings.$pkg-prefix}--apikey-modal;
.#{$block-class}__error-icon svg {
fill: $button-danger-primary;
}
+
+.#{$block-class}__checkmark-icon {
+ fill: $button-primary;
+}
diff --git a/packages/ibm-products/src/components/APIKeyModal/APIKeyDownloader.js b/packages/ibm-products/src/components/APIKeyModal/APIKeyDownloader.js
index b947a58e10..2299d6b863 100644
--- a/packages/ibm-products/src/components/APIKeyModal/APIKeyDownloader.js
+++ b/packages/ibm-products/src/components/APIKeyModal/APIKeyDownloader.js
@@ -64,7 +64,7 @@ APIKeyDownloader.propTypes = {
/**
* body content for the downloader
*/
- body: PropTypes.string.isRequired,
+ body: PropTypes.string,
/**
* aria-label for the download link
*/
diff --git a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.stories.jsx b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.stories.jsx
index f65a7487da..a47e798cc2 100644
--- a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.stories.jsx
+++ b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.stories.jsx
@@ -65,7 +65,7 @@ const defaultProps = {
copyButtonText: 'Copy',
copyIconDescription: 'Copy',
hasAPIKeyVisibilityToggle: true,
- downloadBodyText:
+ helperText:
'This is your unique API key and is non-recoverable. If you lose this API key, you will have to reset it.',
downloadLinkText: 'Download as JSON',
downloadLinkLabel: 'Download API Key in Java Script File format',
@@ -74,8 +74,8 @@ const defaultProps = {
downloadFileType: 'json',
open: true,
closeButtonText: 'Close',
- generateSuccessTitle: 'API key successfully created',
- editSuccessTitle: 'API key successfully saved',
+ generateSuccessMessage: 'API key successfully created',
+ editSuccessMessage: 'API key successfully saved',
loadingText: 'Generating...',
modalLabel: 'An example of Generate API key',
};
@@ -301,7 +301,7 @@ const MultiStepTemplate = (args) => {
)}
{editSuccess && (
- Edited successfully
+ Edited successfully, API key successfully saved.
)}
>
@@ -415,6 +415,7 @@ export const InstantGenerate = InstantTemplate.bind({});
InstantGenerate.args = {
...defaultProps,
apiKeyLabel: 'Unique API Key',
+ generateTitle: 'Generate an API key',
};
export const CustomGenerate = MultiStepTemplate.bind({});
@@ -428,6 +429,7 @@ CustomGenerate.args = {
savedAllResources: false,
savedResource: '',
savedPermissions: '',
+ generateTitle: 'Generate an API key',
};
CustomGenerate.parameters = {
docs: {
@@ -489,6 +491,7 @@ CustomEdit.args = {
savedPermissions: 'Read only',
editing: true,
editButtonText: 'Save API key',
+ generateTitle: 'Save an API key',
};
CustomEdit.parameters = {
docs: {
diff --git a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.test.js b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.test.js
index 0e69271bec..f0833c35ce 100644
--- a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.test.js
+++ b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.test.js
@@ -36,20 +36,20 @@ const defaultProps = {
copyButtonText: 'copy',
copyIconDescription: 'copy icon description',
customSteps: [],
- downloadBodyText: 'download body',
+ helperText: 'download body',
downloadFileName: 'filename',
downloadFileType: 'json',
downloadLinkText: 'download',
downloadLinkLabel: 'Download API Key in Java Script File format',
editButtonText: 'edit button',
editSuccess: false,
- editSuccessTitle: 'edited successfully',
+ editSuccessMessage: 'edited successfully',
editing: false,
error: false,
errorText: 'an error occurred',
generateButtonText: 'create button',
generateSuccessBody: 'created successfully body',
- generateSuccessTitle: 'created successfully title',
+ generateSuccessMessage: 'created successfully title',
generateTitle: 'create title',
hasAPIKeyVisibilityToggle: true,
hasDownloadLink: true,
@@ -135,7 +135,7 @@ describe(componentName, () => {
getByText(props.loadingText, { selector: 'div' });
rerender();
await waitFor(() => getByText(props.downloadLinkLabel));
- getByText(props.downloadBodyText);
+ getByText(props.helperText);
const modal = getByRole('presentation');
expect(modal.querySelector(`.${carbon.prefix}--text-input`).value).toBe(
'444-444-444-444'
@@ -201,7 +201,7 @@ describe(componentName, () => {
customSteps,
hasDownloadLink: false,
};
- const { rerender, getByPlaceholderText, getByText } = render(
+ const { rerender, getByPlaceholderText, getByText, getAllByText } = render(
);
@@ -252,7 +252,7 @@ describe(componentName, () => {
rerender();
expect(screen.getByLabelText(props.apiKeyLabel).value).toBe('abc-123');
getByText(props.generateSuccessBody);
- getByText(props.generateSuccessTitle);
+ getAllByText(props.generateSuccessMessage);
await act(() => click(getByText(props.closeButtonText)));
expect(onClose).toHaveBeenCalled();
});
@@ -361,7 +361,7 @@ describe(componentName, () => {
onRequestEdit,
};
- const { getByText, getByRole, rerender } = render(
+ const { getByText, getAllByText, getByRole, rerender } = render(
);
@@ -373,7 +373,7 @@ describe(componentName, () => {
await act(() => click(editButton));
expect(onRequestEdit).toHaveBeenCalledWith(nameInput.value);
rerender();
- getByText(props.editSuccessTitle);
+ getAllByText(props.editSuccessMessage);
});
it('toggles key visibility', async () => {
diff --git a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.tsx b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.tsx
index c7e0809c97..9b1548c4e7 100644
--- a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.tsx
+++ b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.tsx
@@ -26,7 +26,12 @@ import {
Button,
unstable_FeatureFlags as FeatureFlags,
} from '@carbon/react';
-import { InformationFilled, Copy, ErrorFilled } from '@carbon/react/icons';
+import {
+ InformationFilled,
+ Copy,
+ ErrorFilled,
+ CheckmarkFilled,
+} from '@carbon/react/icons';
import { APIKeyDownloader } from './APIKeyDownloader';
import { pkg } from '../../settings';
import { usePortalTarget } from '../../global/js/hooks/usePortalTarget';
@@ -68,12 +73,14 @@ export let APIKeyModal: React.FC = forwardRef(
editButtonText,
editSuccess,
editSuccessTitle,
+ editSuccessMessage,
editing,
error,
errorText,
generateButtonText,
generateSuccessBody,
generateSuccessTitle,
+ generateSuccessMessage,
generateTitle,
hasAPIKeyVisibilityToggle,
hasDownloadLink,
@@ -96,6 +103,7 @@ export let APIKeyModal: React.FC = forwardRef(
previousStepButtonText,
selectorPrimaryFocus,
showAPIKeyLabel,
+ helperText,
// Collect any other property values passed in.
...rest
@@ -103,6 +111,9 @@ export let APIKeyModal: React.FC = forwardRef(
ref: React.Ref
) => {
const [title, setTitle] = useState(null);
+ const [successMessage, setSuccessMessage] = useState<
+ string | null | undefined
+ >(null);
const [copyError, setCopyError] = useState(false);
const [name, setName] = useState(apiKeyName);
const [currentStep, setCurrentStep] = useState(0);
@@ -121,6 +132,7 @@ export let APIKeyModal: React.FC = forwardRef(
};
const blockClass = `${pkg.prefix}--apikey-modal`;
const localRef = useRef(undefined);
+ const PasswordInputRef = useRef(null);
const modalRef = (ref || localRef) as MutableRefObject;
const { firstElement, keyDownListener } = useFocus(
modalRef,
@@ -132,6 +144,9 @@ export let APIKeyModal: React.FC = forwardRef(
if (copyRef.current && open && apiKeyLoaded) {
copyRef.current.focus();
}
+ if (PasswordInputRef?.current) {
+ PasswordInputRef?.current.setAttribute('readOnly', 'true');
+ }
}, [open, apiKeyLoaded]);
useEffect(() => {
@@ -184,9 +199,11 @@ export let APIKeyModal: React.FC = forwardRef(
useEffect(() => {
if (editing && editSuccess) {
- setTitle(editSuccessTitle);
+ setTitle(generateTitle);
+ setSuccessMessage(editSuccessMessage ?? editSuccessTitle);
} else if (apiKeyLoaded) {
- setTitle(generateSuccessTitle);
+ setTitle(generateTitle);
+ setSuccessMessage(generateSuccessMessage ?? generateSuccessTitle);
} else if (hasSteps) {
setTitle(customSteps[currentStep]?.title);
} else {
@@ -194,11 +211,14 @@ export let APIKeyModal: React.FC = forwardRef(
}
}, [
apiKeyLoaded,
+ loading,
editing,
editSuccess,
editSuccessTitle,
+ editSuccessMessage,
hasSteps,
generateSuccessTitle,
+ generateSuccessMessage,
generateTitle,
currentStep,
customSteps,
@@ -276,6 +296,8 @@ export let APIKeyModal: React.FC = forwardRef(
showPasswordLabel={showAPIKeyLabel}
hidePasswordLabel={hideAPIKeyLabel}
tooltipPosition="left"
+ helperText={helperText}
+ ref={PasswordInputRef}
/>
)}
{!editing && apiKey && !hasAPIKeyVisibilityToggle && (
@@ -314,7 +336,11 @@ export let APIKeyModal: React.FC = forwardRef(
-
+
{copyError ? copyErrorText : errorText}
@@ -332,12 +358,32 @@ export let APIKeyModal: React.FC = forwardRef(
downloadLinkLabel={downloadLinkLabel}
/>
) : (
-
+
{generateSuccessBody}
)}
)}
+
+ {(editSuccess || (apiKeyLoaded && successMessage)) && (
+
+
+
+ {successMessage}
+
+
+ )}
>
)}
@@ -376,6 +422,20 @@ const downloadRequiredProps = (type) =>
// Return a placeholder if not released and not enabled by feature flag
APIKeyModal = pkg.checkComponentEnabled(APIKeyModal, componentName);
+export const deprecatedProps = {
+ /**
+ * deprecated
+ * title for a successful edit
+ */
+ editSuccessTitle: PropTypes.string,
+
+ /**
+ * deprecated
+ * title for a successful key generation
+ */
+ generateSuccessTitle: PropTypes.string,
+};
+
APIKeyModal.propTypes = {
/**
* the api key that's displayed to the user when a request to create is fulfilled.
@@ -436,7 +496,7 @@ APIKeyModal.propTypes = {
/**
* the content that appears that indicates the key is downloadable
*/
- downloadBodyText: downloadRequiredProps(PropTypes.string),
+ downloadBodyText: PropTypes.string,
/**
* designates the name of downloadable json file with the key. if not specified will default to 'apikey'
*/
@@ -464,7 +524,7 @@ APIKeyModal.propTypes = {
/**
* title for a successful edit
*/
- editSuccessTitle: editRequiredProps(PropTypes.string),
+ editSuccessMessage: editRequiredProps(PropTypes.string),
/**
* designates if the modal is in the edit mode
*/
@@ -490,7 +550,7 @@ APIKeyModal.propTypes = {
/**
* title for a successful key generation
*/
- generateSuccessTitle: PropTypes.string,
+ generateSuccessMessage: PropTypes.string,
/**
* default title for the modal in generate key mode
*/
@@ -503,6 +563,10 @@ APIKeyModal.propTypes = {
* designates if user is able to download the api key
*/
hasDownloadLink: PropTypes.bool,
+ /**
+ * helper text for password input
+ */
+ helperText: PropTypes.string,
/**
* label text that's displayed when hovering over visibility toggler to hide key
*/
@@ -582,6 +646,8 @@ APIKeyModal.propTypes = {
* label text that's displayed when hovering over visibility toggler to show key
*/
showAPIKeyLabel: PropTypes.string,
+
+ ...deprecatedProps,
};
APIKeyModal.displayName = componentName;
diff --git a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts
index 200528760a..16233bbd7e 100644
--- a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts
+++ b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts
@@ -75,9 +75,14 @@ interface APIKeyModalCommonProps {
*/
generateSuccessBody?: ReactNode;
/**
+ * * @deprecated use `generateSuccessMessage` instead
* title for a successful key generation
*/
generateSuccessTitle?: string;
+ /**
+ * success message for a successful key generation
+ */
+ generateSuccessMessage?: string;
/**
* default title for the modal in generate key mode
*/
@@ -160,6 +165,10 @@ interface APIKeyModalCommonProps {
* label text that's displayed when hovering over visibility toggler to show key
*/
showAPIKeyLabel?: string;
+ /**
+ * helper text for password input
+ */
+ helperText?: string;
}
type CustomStepConditionalProps = {
@@ -195,9 +204,14 @@ type EditingConditionalProps = {
*/
editSuccess: boolean;
/**
+ * * @deprecated use `editSuccessMessage` instead
* title for a successful edit
*/
- editSuccessTitle: string;
+ editSuccessTitle?: string;
+ /**
+ * success message for edit
+ */
+ editSuccessMessage: string;
};
type HasDownloadLinkProps = {
@@ -208,7 +222,7 @@ type HasDownloadLinkProps = {
/**
* the content that appears that indicates the key is downloadable
*/
- downloadBodyText: string;
+ downloadBodyText?: string;
/**
* designates the name of downloadable json file with the key. if not specified will default to 'apikey'
*/