mirror of
https://github.com/grafana/grafana.git
synced 2024-12-23 07:34:08 -06:00
Login: Improve accessibility of Login form (#78652)
* Chore: Fix a11y debt in Login form * fix tests * token styles * more styles * pa11y * fix pa11y
This commit is contained in:
parent
7dbbdc16a3
commit
eea35b9eb7
@ -1139,9 +1139,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||
],
|
||||
"public/app/core/components/ForgottenPassword/ChangePassword.tsx:5381": [
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||
],
|
||||
"public/app/core/components/ForgottenPassword/ForgottenPassword.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
],
|
||||
@ -1178,38 +1175,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "4"]
|
||||
],
|
||||
"public/app/core/components/Login/LoginForm.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "2"],
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "3"]
|
||||
],
|
||||
"public/app/core/components/Login/LoginLayout.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "5"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "6"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "7"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "8"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "9"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "10"]
|
||||
],
|
||||
"public/app/core/components/Login/LoginPage.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
],
|
||||
"public/app/core/components/Login/LoginServiceButtons.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "4"]
|
||||
],
|
||||
"public/app/core/components/Login/UserSignup.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
],
|
||||
"public/app/core/components/NestedFolderPicker/Trigger.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
],
|
||||
@ -1331,9 +1296,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||
],
|
||||
"public/app/core/components/PasswordField/PasswordField.tsx:5381": [
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||
],
|
||||
"public/app/core/components/QueryOperationRow/OperationRowHelp.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
],
|
||||
|
@ -72,8 +72,8 @@ var config = {
|
||||
"wait for element input[name='user'] to be added",
|
||||
"set field input[name='user'] to admin",
|
||||
"set field input[name='password'] to admin",
|
||||
"click element button[aria-label='Login button']",
|
||||
"wait for element [aria-label='Skip change password button'] to be visible",
|
||||
"click element button[data-testid='data-testid Login button']",
|
||||
"wait for element button[data-testid='data-testid Skip change password button'] to be visible",
|
||||
],
|
||||
threshold: 15,
|
||||
rootElement: '.main-view',
|
||||
|
@ -61,8 +61,8 @@ var config = {
|
||||
"wait for element input[name='user'] to be added",
|
||||
"set field input[name='user'] to admin",
|
||||
"set field input[name='password'] to admin",
|
||||
"click element button[aria-label='Login button']",
|
||||
"wait for element [aria-label='Skip change password button'] to be visible",
|
||||
"click element button[data-testid='data-testid Login button']",
|
||||
"wait for element button[data-testid='data-testid Skip change password button'] to be visible",
|
||||
],
|
||||
wait: 500,
|
||||
rootElement: '.main-view',
|
||||
|
@ -8,10 +8,10 @@ import { Components } from './components';
|
||||
export const Pages = {
|
||||
Login: {
|
||||
url: '/login',
|
||||
username: 'Username input field',
|
||||
password: 'Password input field',
|
||||
submit: 'Login button',
|
||||
skip: 'Skip change password button',
|
||||
username: 'data-testid Username input field',
|
||||
password: 'data-testid Password input field',
|
||||
submit: 'data-testid Login button',
|
||||
skip: 'data-testid Skip change password button',
|
||||
},
|
||||
Home: {
|
||||
url: '/',
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { SyntheticEvent } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Tooltip, Form, Field, VerticalGroup, Button, Alert } from '@grafana/ui';
|
||||
import { Tooltip, Form, Field, VerticalGroup, Button, Alert, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { submitButton } from '../Login/LoginForm';
|
||||
import { getStyles } from '../Login/LoginForm';
|
||||
import { PasswordField } from '../PasswordField/PasswordField';
|
||||
interface Props {
|
||||
onSubmit: (pw: string) => void;
|
||||
@ -17,6 +17,7 @@ interface PasswordDTO {
|
||||
}
|
||||
|
||||
export const ChangePassword = ({ onSubmit, onSkip, showDefaultPasswordWarning }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const submit = (passwords: PasswordDTO) => {
|
||||
onSubmit(passwords.newPassword);
|
||||
};
|
||||
@ -29,24 +30,24 @@ export const ChangePassword = ({ onSubmit, onSkip, showDefaultPasswordWarning }:
|
||||
)}
|
||||
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
|
||||
<PasswordField
|
||||
{...register('newPassword', { required: 'New Password is required' })}
|
||||
id="new-password"
|
||||
autoFocus
|
||||
autoComplete="new-password"
|
||||
{...register('newPassword', { required: 'New Password is required' })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
|
||||
<PasswordField
|
||||
id="confirm-new-password"
|
||||
autoComplete="new-password"
|
||||
{...register('confirmNew', {
|
||||
required: 'Confirmed Password is required',
|
||||
validate: (v: string) => v === getValues().newPassword || 'Passwords must match!',
|
||||
})}
|
||||
id="confirm-new-password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Field>
|
||||
<VerticalGroup>
|
||||
<Button type="submit" className={submitButton}>
|
||||
<Button type="submit" className={styles.submitButton}>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
@ -55,7 +56,7 @@ export const ChangePassword = ({ onSubmit, onSkip, showDefaultPasswordWarning }:
|
||||
content="If you skip you will be prompted to change password next time you log in."
|
||||
placement="bottom"
|
||||
>
|
||||
<Button fill="text" onClick={onSkip} type="button" aria-label={selectors.pages.Login.skip}>
|
||||
<Button fill="text" onClick={onSkip} type="button" data-testid={selectors.pages.Login.skip}>
|
||||
Skip
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { ReactElement } from 'react';
|
||||
import React, { ReactElement, useId } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Button, Form, Input, Field } from '@grafana/ui';
|
||||
import { Button, Form, Input, Field, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PasswordField } from '../PasswordField/PasswordField';
|
||||
|
||||
@ -16,43 +17,38 @@ interface Props {
|
||||
loginHint: string;
|
||||
}
|
||||
|
||||
const wrapperStyles = css`
|
||||
width: 100%;
|
||||
padding-bottom: 16px;
|
||||
`;
|
||||
|
||||
export const submitButton = css`
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LoginForm = ({ children, onSubmit, isLoggingIn, passwordHint, loginHint }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const usernameId = useId();
|
||||
const passwordId = useId();
|
||||
|
||||
return (
|
||||
<div className={wrapperStyles}>
|
||||
<div className={styles.wrapper}>
|
||||
<Form onSubmit={onSubmit} validateOn="onChange">
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
<Field label="Email or username" invalid={!!errors.user} error={errors.user?.message}>
|
||||
<Input
|
||||
{...register('user', { required: 'Email or username is required' })}
|
||||
id={usernameId}
|
||||
autoFocus
|
||||
autoCapitalize="none"
|
||||
placeholder={loginHint}
|
||||
aria-label={selectors.pages.Login.username}
|
||||
data-testid={selectors.pages.Login.username}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Password" invalid={!!errors.password} error={errors.password?.message}>
|
||||
<PasswordField
|
||||
id="current-password"
|
||||
autoComplete="current-password"
|
||||
passwordHint={passwordHint}
|
||||
{...register('password', { required: 'Password is required' })}
|
||||
id={passwordId}
|
||||
autoComplete="current-password"
|
||||
placeholder={passwordHint}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
type="submit"
|
||||
aria-label={selectors.pages.Login.submit}
|
||||
className={submitButton}
|
||||
data-testid={selectors.pages.Login.submit}
|
||||
className={styles.submitButton}
|
||||
disabled={isLoggingIn}
|
||||
>
|
||||
{isLoggingIn ? 'Logging in...' : 'Log in'}
|
||||
@ -64,3 +60,17 @@ export const LoginForm = ({ children, onSubmit, isLoggingIn, passwordHint, login
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
wrapper: css({
|
||||
width: '100%',
|
||||
paddingBottom: theme.spacing(2),
|
||||
}),
|
||||
|
||||
submitButton: css({
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import { cx, css, keyframes } from '@emotion/css';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, styleMixins } from '@grafana/ui';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Branding } from '../Branding/Branding';
|
||||
import { BrandingSettings } from '../Branding/types';
|
||||
@ -92,90 +92,90 @@ export const getLoginStyles = (theme: GrafanaTheme2) => {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}),
|
||||
loginAnim: css`
|
||||
&:before {
|
||||
opacity: 1;
|
||||
}
|
||||
loginAnim: css({
|
||||
['&:before']: {
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
.login-content-box {
|
||||
opacity: 1;
|
||||
}
|
||||
`,
|
||||
submitButton: css`
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`,
|
||||
loginLogo: css`
|
||||
width: 100%;
|
||||
max-width: 60px;
|
||||
margin-bottom: 15px;
|
||||
['.login-content-box']: {
|
||||
opacity: 1,
|
||||
},
|
||||
}),
|
||||
submitButton: css({
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
}),
|
||||
loginLogo: css({
|
||||
width: '100%',
|
||||
maxWidth: 60,
|
||||
marginBottom: theme.spacing(2),
|
||||
|
||||
@media ${styleMixins.mediaUp(theme.v1.breakpoints.sm)} {
|
||||
max-width: 100px;
|
||||
}
|
||||
`,
|
||||
loginLogoWrapper: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: ${theme.spacing(3)};
|
||||
`,
|
||||
titleWrapper: css`
|
||||
text-align: center;
|
||||
`,
|
||||
mainTitle: css`
|
||||
font-size: 22px;
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
maxWidth: 100,
|
||||
},
|
||||
}),
|
||||
loginLogoWrapper: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(3),
|
||||
}),
|
||||
titleWrapper: css({
|
||||
textAlign: 'center',
|
||||
}),
|
||||
mainTitle: css({
|
||||
fontSize: 22,
|
||||
|
||||
@media ${styleMixins.mediaUp(theme.v1.breakpoints.sm)} {
|
||||
font-size: 32px;
|
||||
}
|
||||
`,
|
||||
subTitle: css`
|
||||
font-size: ${theme.typography.size.md};
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
loginContent: css`
|
||||
max-width: 478px;
|
||||
width: calc(100% - 2rem);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
justify-content: flex-start;
|
||||
z-index: 1;
|
||||
min-height: 320px;
|
||||
border-radius: ${theme.shape.borderRadius(4)};
|
||||
padding: ${theme.spacing(2, 0)};
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
fontSize: 32,
|
||||
},
|
||||
}),
|
||||
subTitle: css({
|
||||
fontSize: theme.typography.size.md,
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
loginContent: css({
|
||||
maxWidth: 478,
|
||||
width: `calc(100% - 2rem)`,
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
justifyContent: 'flex-start',
|
||||
zIndex: 1,
|
||||
minHeight: 320,
|
||||
borderRadius: theme.shape.borderRadius(4),
|
||||
padding: theme.spacing(2, 0),
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
|
||||
@media ${styleMixins.mediaUp(theme.v1.breakpoints.sm)} {
|
||||
min-height: 320px;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
loginOuterBox: css`
|
||||
display: flex;
|
||||
overflow-y: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
loginInnerBox: css`
|
||||
padding: ${theme.spacing(0, 2, 2, 2)};
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
minHeight: theme.spacing(40),
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}),
|
||||
loginOuterBox: css({
|
||||
display: 'flex',
|
||||
overflowU: 'hidden',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}),
|
||||
loginInnerBox: css({
|
||||
padding: theme.spacing(0, 2, 2, 2),
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
max-width: 415px;
|
||||
width: 100%;
|
||||
transform: translate(0px, 0px);
|
||||
transition: 0.25s ease;
|
||||
`,
|
||||
enterAnimation: css`
|
||||
animation: ${flyInAnimation} ease-out 0.2s;
|
||||
`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexGrow: 1,
|
||||
maxWidth: 415,
|
||||
width: '100%',
|
||||
transform: 'translate(0px, 0px)',
|
||||
transition: '0.25s ease',
|
||||
}),
|
||||
enterAnimation: css({
|
||||
animation: `${flyInAnimation} ease-out 0.2s`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -39,9 +39,9 @@ describe('Login Page', () => {
|
||||
render(<LoginPage />);
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Welcome to Grafana' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: 'Username input field' })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Password input field')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Login button' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: 'Email or username' })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Forgot your password?' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Forgot your password?' })).toHaveAttribute(
|
||||
@ -56,20 +56,20 @@ describe('Login Page', () => {
|
||||
it('should pass validation checks for username field', async () => {
|
||||
render(<LoginPage />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Login button' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
|
||||
expect(await screen.findByText('Email or username is required')).toBeInTheDocument();
|
||||
|
||||
await userEvent.type(screen.getByRole('textbox', { name: 'Username input field' }), 'admin');
|
||||
await userEvent.type(screen.getByRole('textbox', { name: 'Email or username' }), 'admin');
|
||||
await waitFor(() => expect(screen.queryByText('Email or username is required')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should pass validation checks for password field', async () => {
|
||||
render(<LoginPage />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Login button' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
|
||||
expect(await screen.findByText('Password is required')).toBeInTheDocument();
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Password input field'), 'admin');
|
||||
await userEvent.type(screen.getByLabelText('Password'), 'admin');
|
||||
await waitFor(() => expect(screen.queryByText('Password is required')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
@ -82,9 +82,9 @@ describe('Login Page', () => {
|
||||
postMock.mockResolvedValueOnce({ message: 'Logged in' });
|
||||
render(<LoginPage />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Username input field'), 'admin');
|
||||
await userEvent.type(screen.getByLabelText('Password input field'), 'test');
|
||||
fireEvent.click(screen.getByLabelText('Login button'));
|
||||
await userEvent.type(screen.getByLabelText('Email or username'), 'admin');
|
||||
await userEvent.type(screen.getByLabelText('Password'), 'test');
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(postMock).toHaveBeenCalledWith('/login', { password: 'test', user: 'admin' }, { showErrorAlert: false })
|
||||
@ -128,9 +128,9 @@ describe('Login Page', () => {
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Username input field'), 'admin');
|
||||
await userEvent.type(screen.getByLabelText('Password input field'), 'test');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Login button' }));
|
||||
await userEvent.type(screen.getByLabelText('Email or username'), 'admin');
|
||||
await userEvent.type(screen.getByLabelText('Password'), 'test');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Log in' }));
|
||||
|
||||
const alert = await screen.findByRole('alert', { name: 'Login failed' });
|
||||
expect(alert).toBeInTheDocument();
|
||||
@ -150,9 +150,9 @@ describe('Login Page', () => {
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Username input field'), 'admin');
|
||||
await userEvent.type(screen.getByLabelText('Password input field'), 'test');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Login button' }));
|
||||
await userEvent.type(screen.getByLabelText('Email or username'), 'admin');
|
||||
await userEvent.type(screen.getByLabelText('Password'), 'test');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Log in' }));
|
||||
|
||||
const alert = await screen.findByRole('alert', { name: 'Login failed' });
|
||||
expect(alert).toBeInTheDocument();
|
||||
|
@ -3,7 +3,8 @@ import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
// Components
|
||||
import { Alert, HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { Branding } from 'app/core/components/Branding/Branding';
|
||||
import config from 'app/core/config';
|
||||
import { t } from 'app/core/internationalization';
|
||||
@ -16,17 +17,10 @@ import { LoginLayout, InnerBox } from './LoginLayout';
|
||||
import { LoginServiceButtons } from './LoginServiceButtons';
|
||||
import { UserSignup } from './UserSignup';
|
||||
|
||||
const forgottenPasswordStyles = css`
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
`;
|
||||
|
||||
const alertStyles = css({
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const LoginPage = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
document.title = Branding.AppTitle;
|
||||
|
||||
return (
|
||||
<LoginCtrl>
|
||||
{({
|
||||
@ -46,7 +40,7 @@ export const LoginPage = () => {
|
||||
{!isChangingPassword && (
|
||||
<InnerBox>
|
||||
{loginErrorMessage && (
|
||||
<Alert className={alertStyles} severity="error" title={t('login.error.title', 'Login failed')}>
|
||||
<Alert className={styles.alert} severity="error" title={t('login.error.title', 'Login failed')}>
|
||||
{loginErrorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
@ -55,7 +49,7 @@ export const LoginPage = () => {
|
||||
<LoginForm onSubmit={login} loginHint={loginHint} passwordHint={passwordHint} isLoggingIn={isLoggingIn}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<LinkButton
|
||||
className={forgottenPasswordStyles}
|
||||
className={styles.forgottenPassword}
|
||||
fill="text"
|
||||
href={`${config.appSubUrl}/user/password/send-reset-email`}
|
||||
>
|
||||
@ -83,3 +77,16 @@ export const LoginPage = () => {
|
||||
</LoginCtrl>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
forgottenPassword: css({
|
||||
padding: 0,
|
||||
marginTop: theme.spacing(0.5),
|
||||
}),
|
||||
|
||||
alert: css({
|
||||
width: '100%',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -77,30 +77,30 @@ const loginServices: () => LoginServices = () => {
|
||||
|
||||
const getServiceStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
button: css`
|
||||
color: #d8d9da;
|
||||
position: relative;
|
||||
`,
|
||||
buttonIcon: css`
|
||||
position: absolute;
|
||||
left: ${theme.spacing(1)};
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`,
|
||||
button: css({
|
||||
color: '#d8d9da',
|
||||
position: 'relative',
|
||||
}),
|
||||
buttonIcon: css({
|
||||
position: 'absolute',
|
||||
left: theme.spacing(1),
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
}),
|
||||
divider: {
|
||||
base: css`
|
||||
color: ${theme.colors.text};
|
||||
display: flex;
|
||||
margin-bottom: ${theme.spacing(1)};
|
||||
justify-content: space-between;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
`,
|
||||
line: css`
|
||||
width: 100px;
|
||||
height: 10px;
|
||||
border-bottom: 1px solid ${theme.colors.text};
|
||||
`,
|
||||
base: css({
|
||||
color: theme.colors.text.primary,
|
||||
display: 'flex',
|
||||
marginBottom: theme.spacing(1),
|
||||
justifyContent: 'space-between',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
}),
|
||||
line: css({
|
||||
width: 100,
|
||||
height: 10,
|
||||
borderBottom: `1px solid ${theme.colors.text}`,
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -128,15 +128,15 @@ const LoginDivider = () => {
|
||||
function getButtonStyleFor(service: LoginService, styles: ReturnType<typeof getServiceStyles>, theme: GrafanaTheme2) {
|
||||
return cx(
|
||||
styles.button,
|
||||
css`
|
||||
background-color: ${service.bgColor};
|
||||
color: ${theme.colors.getContrastText(service.bgColor)};
|
||||
css({
|
||||
backgroundColor: service.bgColor,
|
||||
color: theme.colors.getContrastText(service.bgColor),
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.colors.emphasize(service.bgColor, 0.15)};
|
||||
box-shadow: ${theme.shadows.z1};
|
||||
}
|
||||
`
|
||||
['&:hover']: {
|
||||
backgroundColor: theme.colors.emphasize(service.bgColor, 0.15),
|
||||
boxShadow: theme.shadows.z1,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -12,10 +12,10 @@ export const UserSignup = () => {
|
||||
<VerticalGroup>
|
||||
<div className={paddingTop}>New to Grafana?</div>
|
||||
<LinkButton
|
||||
className={css`
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`}
|
||||
className={css({
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
})}
|
||||
href={href}
|
||||
variant="secondary"
|
||||
fill="outline"
|
||||
|
@ -1,23 +1,31 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Field } from '@grafana/ui';
|
||||
|
||||
import { PasswordField } from './PasswordField';
|
||||
|
||||
describe('PasswordField', () => {
|
||||
const props = {
|
||||
id: 'password',
|
||||
placeholder: 'enter password',
|
||||
'data-testid': 'password-field',
|
||||
};
|
||||
it('should render correctly', () => {
|
||||
render(<PasswordField {...props} />);
|
||||
expect(screen.getByTestId('password-field')).toBeInTheDocument();
|
||||
render(
|
||||
<Field label="Password">
|
||||
<PasswordField id="password" />
|
||||
</Field>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
||||
expect(screen.getByRole('switch', { name: 'Show password' })).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');
|
||||
render(
|
||||
<Field label="Password">
|
||||
<PasswordField id="password" />
|
||||
</Field>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('Password')).toHaveProperty('type', 'password');
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'Show password' }));
|
||||
expect(screen.getByTestId('password-field')).toHaveProperty('type', 'text');
|
||||
expect(screen.getByLabelText('Password')).toHaveProperty('type', 'text');
|
||||
});
|
||||
});
|
||||
|
@ -2,43 +2,33 @@ import React, { useState } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Input, IconButton } from '@grafana/ui';
|
||||
import { Props as InputProps } from '@grafana/ui/src/components/Input/Input';
|
||||
|
||||
export interface Props {
|
||||
autoFocus?: boolean;
|
||||
autoComplete?: string;
|
||||
id?: string;
|
||||
passwordHint?: string;
|
||||
}
|
||||
interface Props extends Omit<InputProps, 'type'> {}
|
||||
|
||||
export const PasswordField = React.forwardRef<HTMLInputElement, Props>(
|
||||
({ autoComplete, autoFocus, id, passwordHint, ...props }, ref) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
export const PasswordField = React.forwardRef<HTMLInputElement, Props>((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'}
|
||||
aria-controls={id}
|
||||
role="switch"
|
||||
aria-checked={showPassword}
|
||||
onClick={() => {
|
||||
setShowPassword(!showPassword);
|
||||
}}
|
||||
tooltip={showPassword ? 'Hide password' : 'Show password'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
data-testid={selectors.pages.Login.password}
|
||||
ref={ref}
|
||||
suffix={
|
||||
<IconButton
|
||||
name={showPassword ? 'eye-slash' : 'eye'}
|
||||
aria-controls={props.id}
|
||||
role="switch"
|
||||
aria-checked={showPassword}
|
||||
onClick={() => {
|
||||
setShowPassword(!showPassword);
|
||||
}}
|
||||
tooltip={showPassword ? 'Hide password' : 'Show password'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
PasswordField.displayName = 'PasswordField';
|
||||
|
Loading…
Reference in New Issue
Block a user