Skip to content

Commit

Permalink
fix(APIKeyModal): improve screen reader announcement (#6481)
Browse files Browse the repository at this point in the history
* fix(APIKeyModal): improve screen reader announcement

* fix(APIKeyModal): without title change

* fix(APIKeyModal): without title change

* fix(APIKeyModal): deprecated props and added style for success message
  • Loading branch information
anamikaanu96 authored Dec 9, 2024
1 parent 6e58e9d commit bea9003
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ APIKeyDownloader.propTypes = {
/**
* body content for the downloader
*/
body: PropTypes.string.isRequired,
body: PropTypes.string,
/**
* aria-label for the download link
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
};
Expand Down Expand Up @@ -301,7 +301,7 @@ const MultiStepTemplate = (args) => {
)}
{editSuccess && (
<div className={`${blockClass}__messaging`}>
Edited successfully
Edited successfully, API key successfully saved.
</div>
)}
</>
Expand Down Expand Up @@ -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({});
Expand All @@ -428,6 +429,7 @@ CustomGenerate.args = {
savedAllResources: false,
savedResource: '',
savedPermissions: '',
generateTitle: 'Generate an API key',
};
CustomGenerate.parameters = {
docs: {
Expand Down Expand Up @@ -489,6 +491,7 @@ CustomEdit.args = {
savedPermissions: 'Read only',
editing: true,
editButtonText: 'Save API key',
generateTitle: 'Save an API key',
};
CustomEdit.parameters = {
docs: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -135,7 +135,7 @@ describe(componentName, () => {
getByText(props.loadingText, { selector: 'div' });
rerender(<APIKeyModal {...props} apiKey="444-444-444-444" />);
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'
Expand Down Expand Up @@ -201,7 +201,7 @@ describe(componentName, () => {
customSteps,
hasDownloadLink: false,
};
const { rerender, getByPlaceholderText, getByText } = render(
const { rerender, getByPlaceholderText, getByText, getAllByText } = render(
<APIKeyModal {...props} />
);

Expand Down Expand Up @@ -252,7 +252,7 @@ describe(componentName, () => {
rerender(<APIKeyModal {...props} apiKey="abc-123" />);
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();
});
Expand Down Expand Up @@ -361,7 +361,7 @@ describe(componentName, () => {
onRequestEdit,
};

const { getByText, getByRole, rerender } = render(
const { getByText, getAllByText, getByRole, rerender } = render(
<APIKeyModal {...props} />
);

Expand All @@ -373,7 +373,7 @@ describe(componentName, () => {
await act(() => click(editButton));
expect(onRequestEdit).toHaveBeenCalledWith(nameInput.value);
rerender(<APIKeyModal {...props} editSuccess />);
getByText(props.editSuccessTitle);
getAllByText(props.editSuccessMessage);
});

it('toggles key visibility', async () => {
Expand Down
82 changes: 74 additions & 8 deletions packages/ibm-products/src/components/APIKeyModal/APIKeyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -68,12 +73,14 @@ export let APIKeyModal: React.FC<APIKeyModalProps> = forwardRef(
editButtonText,
editSuccess,
editSuccessTitle,
editSuccessMessage,
editing,
error,
errorText,
generateButtonText,
generateSuccessBody,
generateSuccessTitle,
generateSuccessMessage,
generateTitle,
hasAPIKeyVisibilityToggle,
hasDownloadLink,
Expand All @@ -96,13 +103,17 @@ export let APIKeyModal: React.FC<APIKeyModalProps> = forwardRef(
previousStepButtonText,
selectorPrimaryFocus,
showAPIKeyLabel,
helperText,

// Collect any other property values passed in.
...rest
},
ref: React.Ref<HTMLDivElement>
) => {
const [title, setTitle] = useState<string | null | undefined>(null);
const [successMessage, setSuccessMessage] = useState<
string | null | undefined
>(null);
const [copyError, setCopyError] = useState(false);
const [name, setName] = useState(apiKeyName);
const [currentStep, setCurrentStep] = useState(0);
Expand All @@ -121,6 +132,7 @@ export let APIKeyModal: React.FC<APIKeyModalProps> = forwardRef(
};
const blockClass = `${pkg.prefix}--apikey-modal`;
const localRef = useRef(undefined);
const PasswordInputRef = useRef<HTMLElement | null>(null);
const modalRef = (ref || localRef) as MutableRefObject<HTMLDivElement>;
const { firstElement, keyDownListener } = useFocus(
modalRef,
Expand All @@ -132,6 +144,9 @@ export let APIKeyModal: React.FC<APIKeyModalProps> = forwardRef(
if (copyRef.current && open && apiKeyLoaded) {
copyRef.current.focus();
}
if (PasswordInputRef?.current) {
PasswordInputRef?.current.setAttribute('readOnly', 'true');
}
}, [open, apiKeyLoaded]);

useEffect(() => {
Expand Down Expand Up @@ -184,21 +199,26 @@ export let APIKeyModal: React.FC<APIKeyModalProps> = 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 {
setTitle(generateTitle);
}
}, [
apiKeyLoaded,
loading,
editing,
editSuccess,
editSuccessTitle,
editSuccessMessage,
hasSteps,
generateSuccessTitle,
generateSuccessMessage,
generateTitle,
currentStep,
customSteps,
Expand Down Expand Up @@ -276,6 +296,8 @@ export let APIKeyModal: React.FC<APIKeyModalProps> = forwardRef(
showPasswordLabel={showAPIKeyLabel}
hidePasswordLabel={hideAPIKeyLabel}
tooltipPosition="left"
helperText={helperText}
ref={PasswordInputRef}
/>
)}
{!editing && apiKey && !hasAPIKeyVisibilityToggle && (
Expand Down Expand Up @@ -314,7 +336,11 @@ export let APIKeyModal: React.FC<APIKeyModalProps> = forwardRef(
<div className={`${blockClass}__error-icon`}>
<ErrorFilled size={16} />
</div>
<p className={`${blockClass}__messaging-text`}>
<p
className={`${blockClass}__messaging-text`}
role="alert"
aria-live="assertive"
>
{copyError ? copyErrorText : errorText}
</p>
</div>
Expand All @@ -332,12 +358,32 @@ export let APIKeyModal: React.FC<APIKeyModalProps> = forwardRef(
downloadLinkLabel={downloadLinkLabel}
/>
) : (
<div className={`${blockClass}__messaging-text`}>
<div
className={`${blockClass}__messaging-text`}
role="alert"
aria-live="assertive"
>
{generateSuccessBody}
</div>
)}
</div>
)}

{(editSuccess || (apiKeyLoaded && successMessage)) && (
<div className={`${blockClass}__messaging`}>
<CheckmarkFilled
size={16}
className={`${blockClass}__checkmark-icon`}
/>
<p
className={`${blockClass}__messaging-text`}
role="alert"
aria-live="assertive"
>
{successMessage}
</p>
</div>
)}
</>
)}
</ModalBody>
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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'
*/
Expand Down Expand Up @@ -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
*/
Expand All @@ -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
*/
Expand All @@ -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
*/
Expand Down Expand Up @@ -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;
Loading

0 comments on commit bea9003

Please sign in to comment.