diff --git a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.stories.jsx b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.stories.jsx index 28dd6f748f..f65a7487da 100644 --- a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.stories.jsx +++ b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.stories.jsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { Button, TextInput, @@ -85,6 +85,7 @@ const blockClass = `${pkg.prefix}--apikey-modal`; const InstantTemplate = (args) => { const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); + const buttonRef = useRef(undefined); const generateKey = async () => { setLoading(true); @@ -96,7 +97,12 @@ const InstantTemplate = (args) => { return ( <> - setOpen(false)} open={open} /> + setOpen(false)} + open={open} + launcherButtonRef={buttonRef} + /> {loading ? ( ) : ( - + )} ); @@ -117,6 +125,7 @@ const TemplateWithState = (args) => { const [apiKey, setApiKey] = useState(''); const [loading, setLoading] = useState(false); const [fetchError, setFetchError] = useState(false); + const buttonRef = useRef(undefined); // eslint-disable-next-line const submitHandler = async (apiKeyName) => { @@ -148,8 +157,11 @@ const TemplateWithState = (args) => { onRequestGenerate={submitHandler} open={open} error={fetchError} + launcherButtonRef={buttonRef} /> - + ); }; @@ -166,6 +178,7 @@ const MultiStepTemplate = (args) => { const [open, setOpen] = useState(false); const [apiKey, setApiKey] = useState(''); const [loading, setLoading] = useState(false); + const buttonRef = useRef(undefined); // multi step options const [name, setName] = useState(savedName); @@ -310,8 +323,9 @@ const MultiStepTemplate = (args) => { customSteps={steps} nameRequired={false} editSuccess={editSuccess} + launcherButtonRef={buttonRef} /> - @@ -324,6 +338,7 @@ const EditTemplate = (args) => { const [loading, setLoading] = useState(false); const [fetchError, setFetchError] = useState(false); const [fetchSuccess, setFetchSuccess] = useState(false); + const buttonRef = useRef(undefined); const submitHandler = async () => { action(`submitted ${apiKeyName}`)(); @@ -357,8 +372,11 @@ const EditTemplate = (args) => { open={open} error={fetchError} editSuccess={fetchSuccess} + launcherButtonRef={buttonRef} /> - + ); }; diff --git a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.test.js b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.test.js index b46f110f76..0e69271bec 100644 --- a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.test.js +++ b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.test.js @@ -17,6 +17,7 @@ import React from 'react'; import { carbon } from '../../settings'; import { APIKeyModal } from '.'; +import { Button } from '@carbon/react'; Object.assign(navigator, { clipboard: { @@ -302,6 +303,52 @@ describe(componentName, () => { expect(step1InputB).toHaveFocus(); }); + it('should return focus to the generate button', async () => { + const onOpen = jest.fn(() => false); + const onClose = jest.fn(() => true); + + // eslint-disable-next-line react/prop-types + const DummyComponent = ({ open }) => { + const buttonRef = React.useRef(undefined); + + return ( + <> + + + + ); + }; + + const { getByText, rerender } = render(); + + const launchButtonEl = getByText('Generate'); + expect(launchButtonEl).toBeInTheDocument(); + + await act(() => userEvent.click(launchButtonEl)); + expect(onOpen).toHaveBeenCalled(); + + rerender(); + + const closeButton = getByText(defaultProps.closeButtonText); + await act(() => new Promise((resolve) => setTimeout(resolve, 0))); + expect(closeButton).toBeInTheDocument(); + + await act(() => userEvent.click(closeButton)); + expect(onClose).toHaveBeenCalled(); + + rerender(); + + await act(() => new Promise((resolve) => setTimeout(resolve, 0))); + expect(launchButtonEl).toHaveFocus(); + }); + it('successfully edits', async () => { const { change } = fireEvent; const { click } = userEvent; diff --git a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.tsx b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.tsx index 847aed8762..5e2b48217f 100644 --- a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.tsx +++ b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.tsx @@ -35,7 +35,7 @@ import { getDevtoolsProps } from '../../global/js/utils/devtools'; import { isRequiredIf } from '../../global/js/utils/props-helper'; import uuidv4 from '../../global/js/utils/uuidv4'; import { APIKeyModalProps } from './APIKeyModal.types'; -import { useFocus } from '../../global/js/hooks'; +import { useFocus, usePreviousValue } from '../../global/js/hooks'; import { getSpecificElement } from '../../global/js/hooks/useFocus'; const componentName = 'APIKeyModal'; @@ -78,6 +78,7 @@ export let APIKeyModal: React.FC = forwardRef( hasAPIKeyVisibilityToggle, hasDownloadLink, hideAPIKeyLabel, + launcherButtonRef, loading, loadingText, modalLabel, @@ -125,6 +126,7 @@ export let APIKeyModal: React.FC = forwardRef( modalRef, selectorPrimaryFocus ); + const prevOpen = usePreviousValue(open); useEffect(() => { if (copyRef.current && open && apiKeyLoaded) { @@ -159,6 +161,14 @@ export let APIKeyModal: React.FC = forwardRef( } }, [firstElement, modalRef, open, selectorPrimaryFocus]); + useEffect(() => { + if (prevOpen && !open && launcherButtonRef) { + setTimeout(() => { + launcherButtonRef.current.focus(); + }, 0); + } + }, [launcherButtonRef, open, prevOpen]); + const isPrimaryButtonDisabled = () => { if (loading) { return true; @@ -517,6 +527,11 @@ APIKeyModal.propTypes = { * label text that's displayed when hovering over visibility toggler to hide key */ hideAPIKeyLabel: PropTypes.string, + /** + * Provide a ref to return focus to once the tearsheet is closed. + */ + /**@ts-ignore */ + launcherButtonRef: PropTypes.any, /** * designates if the modal is in a loading state via a request or some other in progress operation */ diff --git a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts index 0297bcfd43..200528760a 100644 --- a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts +++ b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { ReactNode } from 'react'; +import { ReactNode, RefObject } from 'react'; interface APIKeyModalCommonProps { /** @@ -94,6 +94,10 @@ interface APIKeyModalCommonProps { * label text that's displayed when hovering over visibility toggler to hide key */ hideAPIKeyLabel?: string; + /** + * Provide a ref to return focus to once the tearsheet is closed. + */ + launcherButtonRef?: RefObject; /** * designates if the modal is in a loading state via a request or some other in progress operation */