Password Field Improvements (#36160)

* Password: added show password functionality
added autcomplete props
created password component

Fixes #28721

* addressed review changes and added unit tests

* wrapped passwordField component in forwardRef

* fix validation and tests
This commit is contained in:
Tharun Rajendran 2021-07-13 12:02:03 +05:30 committed by GitHub
parent dc8874cd2e
commit c26bd6c52f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 97 additions and 31 deletions

View File

@ -1,7 +1,8 @@
import React, { FC, SyntheticEvent } from 'react';
import { Tooltip, Form, Field, Input, VerticalGroup, Button } from '@grafana/ui';
import { Tooltip, Form, Field, VerticalGroup, Button } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { submitButton } from '../Login/LoginForm';
import { PasswordField } from '../PasswordField/PasswordField';
interface Props {
onSubmit: (pw: string) => void;
onSkip?: (event?: SyntheticEvent) => void;
@ -21,21 +22,19 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
{({ errors, register, getValues }) => (
<>
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
<Input
autoFocus
<PasswordField
id="new-password"
type="password"
{...register('newPassword', {
required: 'New password is required',
})}
autoFocus
autoComplete="new-password"
{...register('newPassword', { required: 'New Password is required' })}
/>
</Field>
<Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
<Input
<PasswordField
id="confirm-new-password"
type="password"
autoComplete="new-password"
{...register('confirmNew', {
required: 'Confirmed password is required',
required: 'Confirmed Password is required',
validate: (v: string) => v === getValues().newPassword || 'Passwords must match!',
})}
/>

View File

@ -48,8 +48,8 @@ describe('ChangePassword Page', () => {
render(<ChangePasswordPage {...props} />);
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
expect(await screen.findByText('New password is required')).toBeInTheDocument();
expect(screen.getByText('Confirmed password is required')).toBeInTheDocument();
expect(await screen.findByText('New Password is required')).toBeInTheDocument();
expect(screen.getByText('Confirmed Password is required')).toBeInTheDocument();
await act(async () => {
await userEvent.type(screen.getByLabelText('New password'), 'admin');

View File

@ -4,6 +4,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { FormModel } from './LoginCtrl';
import { Button, Form, Input, Field } from '@grafana/ui';
import { css } from '@emotion/css';
import { PasswordField } from '../PasswordField/PasswordField';
interface Props {
children: ReactElement;
@ -39,11 +40,11 @@ export const LoginForm: FC<Props> = ({ children, onSubmit, isLoggingIn, password
/>
</Field>
<Field label="Password" invalid={!!errors.password} error={errors.password?.message}>
<Input
<PasswordField
id="current-password"
autoComplete="current-password"
passwordHint={passwordHint}
{...register('password', { required: 'Password is required' })}
type="password"
placeholder={passwordHint}
aria-label={selectors.pages.Login.password}
/>
</Field>
<Button aria-label={selectors.pages.Login.submit} className={submitButton} disabled={isLoggingIn}>

View File

@ -0,0 +1,22 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { PasswordField } from './PasswordField';
describe('PasswordField', () => {
const props = {
id: 'password',
placeholder: 'enter password',
'data-testid': 'password-field',
};
it('should renders correctly', () => {
render(<PasswordField {...props} />);
expect(screen.getByTestId('password-field')).toBeInTheDocument();
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('should able to show password value if clicked on password-reveal icon', () => {
render(<PasswordField {...props} />);
expect(screen.getByTestId('password-field')).toHaveProperty('type', 'password');
fireEvent.click(screen.getByRole('button'));
expect(screen.getByTestId('password-field')).toHaveProperty('type', 'text');
});
});

View File

@ -0,0 +1,44 @@
import React, { FC, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Input, IconButton } from '@grafana/ui';
export interface Props {
autoFocus?: boolean;
autoComplete?: string;
id?: string;
passwordHint?: string;
}
export const PasswordField: FC<Props> = React.forwardRef<HTMLInputElement, Props>(
({ autoComplete, autoFocus, id, passwordHint, ...props }, ref) => {
const [showPassword, setShowPassword] = useState(false);
return (
<Input
id={id}
autoFocus={autoFocus}
autoComplete={autoComplete}
{...props}
type={showPassword ? 'text' : 'password'}
placeholder={passwordHint}
aria-label={selectors.pages.Login.password}
ref={ref}
suffix={
<IconButton
name={showPassword ? 'eye-slash' : 'eye'}
surface="header"
aria-controls={id}
aria-expanded={showPassword}
onClick={(e) => {
e.preventDefault();
setShowPassword(!showPassword);
}}
/>
}
/>
);
}
);
PasswordField.displayName = 'PasswordField';

View File

@ -6,6 +6,7 @@ import appEvents from 'app/core/app_events';
import { AppEvents } from '@grafana/data';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { InnerBox, LoginLayout } from '../Login/LoginLayout';
import { PasswordField } from '../PasswordField/PasswordField';
interface SignupDTO {
name?: string;
@ -90,23 +91,21 @@ export const SignupPage: FC<Props> = (props) => {
</Field>
)}
<Field label="Password" invalid={!!errors.password} error={errors?.password?.message}>
<Input
<PasswordField
id="new-password"
{...register('password', {
required: 'Password is required',
})}
autoFocus
type="password"
autoComplete="new-password"
{...register('password', { required: 'Password is required' })}
/>
</Field>
<Field label="Confirm password" invalid={!!errors.confirm} error={errors?.confirm?.message}>
<Input
<PasswordField
id="confirm-new-password"
autoComplete="new-password"
{...register('confirm', {
required: 'Confirmed password is required',
validate: (v) => v === getValues().password || 'Passwords must match!',
})}
type="password"
/>
</Field>

View File

@ -1,10 +1,11 @@
import React, { FC } from 'react';
import { css } from '@emotion/css';
import { Button, Field, Form, HorizontalGroup, Input, LinkButton } from '@grafana/ui';
import { Button, Field, Form, HorizontalGroup, LinkButton } from '@grafana/ui';
import config from 'app/core/config';
import { UserDTO } from 'app/types';
import { ChangePasswordFields } from './types';
import { PasswordField } from '../../core/components/PasswordField/PasswordField';
export interface Props {
user: UserDTO;
@ -34,17 +35,17 @@ export const ChangePasswordForm: FC<Props> = ({ user, onChangePassword, isSaving
return (
<>
<Field label="Old password" invalid={!!errors.oldPassword} error={errors?.oldPassword?.message}>
<Input
id="old-password"
type="password"
<PasswordField
id="current-password"
autoComplete="current-password"
{...register('oldPassword', { required: 'Old password is required' })}
/>
</Field>
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
<Input
<PasswordField
id="new-password"
type="password"
autoComplete="new-password"
{...register('newPassword', {
required: 'New password is required',
validate: {
@ -56,9 +57,9 @@ export const ChangePasswordForm: FC<Props> = ({ user, onChangePassword, isSaving
</Field>
<Field label="Confirm password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
<Input
<PasswordField
id="confirm-new-password"
type="password"
autoComplete="new-password"
{...register('confirmNew', {
required: 'New password confirmation is required',
validate: (v) => v === getValues().newPassword || 'Passwords must match',