mirror of
https://github.com/grafana/grafana.git
synced 2025-01-24 15:27:01 -06:00
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:
parent
dc8874cd2e
commit
c26bd6c52f
@ -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!',
|
||||
})}
|
||||
/>
|
||||
|
@ -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');
|
||||
|
@ -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}>
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
44
public/app/core/components/PasswordField/PasswordField.tsx
Normal file
44
public/app/core/components/PasswordField/PasswordField.tsx
Normal 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';
|
@ -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>
|
||||
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user