diff --git a/web/packages/shared/hooks/useRefAutoFocus/useRefAutoFocus.ts b/web/packages/shared/hooks/useRefAutoFocus/useRefAutoFocus.ts index 7b97905749a3e..c0747a95b9bca 100644 --- a/web/packages/shared/hooks/useRefAutoFocus/useRefAutoFocus.ts +++ b/web/packages/shared/hooks/useRefAutoFocus/useRefAutoFocus.ts @@ -24,6 +24,10 @@ import { DependencyList, MutableRefObject, useEffect, useRef } from 'react'; */ export function useRefAutoFocus(options: { shouldFocus: boolean; + /** + * @deprecated Include items from refocusDeps into the calculation of shouldFocus instead. + * The list of useEffect deps should be statically known. + */ refocusDeps?: DependencyList; }): MutableRefObject { const ref = useRef(undefined); diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.test.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.test.tsx new file mode 100644 index 0000000000000..c42974ed4cac6 --- /dev/null +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.test.tsx @@ -0,0 +1,61 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import userEvent from '@testing-library/user-event'; + +import { render, screen } from 'design/utils/testing'; + +import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; +import { makeRootCluster } from 'teleterm/services/tshd/testHelpers'; +import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider'; +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; + +import { ClusterLogin } from './ClusterLogin'; + +it('keeps the focus on the password field on submission error', async () => { + const user = userEvent.setup(); + const cluster = makeRootCluster(); + const appContext = new MockAppContext(); + appContext.addRootCluster(cluster); + + jest + .spyOn(appContext.tshd, 'login') + .mockResolvedValue( + new MockedUnaryCall(undefined, new Error('whoops something went wrong')) + ); + + render( + + {}} + prefill={{ username: 'alice' }} + reason={undefined} + /> + + ); + + const passwordField = await screen.findByLabelText('Password'); + expect(passwordField).toHaveFocus(); + + await user.type(passwordField, 'foo'); + await user.click(screen.getByText('Sign In')); + + await screen.findByText('whoops something went wrong'); + expect(passwordField).toHaveFocus(); +}); diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx index ec729af33853d..7c8fc24707e72 100644 --- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx @@ -38,11 +38,18 @@ export const FormLocal = ({ const [pass, setPass] = useState(''); const [user, setUser] = useState(loggedInUserName || ''); + // loginAttempt.status needs to be checked here so that auto focus is automatically managed when + // the form reverts from a processing state to an error state. + const isAutoFocusAllowed = + autoFocus && hasTransitionEnded && loginAttempt.status !== 'processing'; + // useRefAutoFocus is used instead of just plain autoFocus because of some weird focus issues + // stemming from, most probably, using within a modal. autoFocus generally works + // within modals. We've never documented why we use this hook so that knowledge is lost in time. const usernameInputRef = useRefAutoFocus({ - shouldFocus: hasTransitionEnded && autoFocus && !loggedInUserName, + shouldFocus: isAutoFocusAllowed && !loggedInUserName, }); const passwordInputRef = useRefAutoFocus({ - shouldFocus: hasTransitionEnded && autoFocus && !!loggedInUserName, + shouldFocus: isAutoFocusAllowed && !!loggedInUserName, }); function onLoginClick(