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' */