Skip to content

Commit

Permalink
OSDEV-1083 Enable Password Visibility on Chrome Browser (#274)
Browse files Browse the repository at this point in the history
[OSDEV-1083](https://opensupplyhub.atlassian.net/browse/OSDEV-1083)
Enable Password Visibility on Chrome Browser.

[OSDEV-1083]:
https://opensupplyhub.atlassian.net/browse/OSDEV-1083?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Implemented 'toggle password visibility' feature in the login, register, reset password
and user profile forms.
This allows users to view their password while typing, helping to
prevent login issues due to misspelled passwords.
  • Loading branch information
VolodymyrVorona authored Jul 12, 2024
1 parent f768013 commit 0e32d22
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 47 deletions.
29 changes: 29 additions & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). The format is based on the `RELEASE-NOTES-TEMPLATE.md` file.


## Release 1.17.0

## Introduction
* Product name: Open Supply Hub
* Release date: July 27, 2024

### Database changes
#### Migrations:
* *Describe migrations here.*

#### Scheme changes
* *Describe scheme changes here.*

### Code/API changes
* *Describe code/API changes here.*

### Architecture/Environment changes
* *Describe architecture/environment changes here.*

### Bugfix
* *Describe bugfix here.*

### What's new
* [OSDEV-1083](https://opensupplyhub.atlassian.net/browse/OSDEV-1083) - Implemented a 'toggle password visibility' feature in the login, registration, reset password and user profile forms.

### Release instructions:
* *Provide release instructions here.*


## Release 1.16.0

## Introduction
Expand Down
62 changes: 62 additions & 0 deletions src/react/src/__tests__/components/TogglePasswordField.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import { screen, fireEvent } from '@testing-library/react';
import TogglePasswordField from '../../components/TogglePasswordField';
import renderWithProviders from '../../util/testUtils/renderWithProviders';

// Mock the styles and icons
jest.mock('@material-ui/icons', () => ({
Visibility: () => <div>Visibility</div>,
VisibilityOff: () => <div>VisibilityOff</div>,
}));

describe('TogglePasswordField', () => {
const defaultProps = {
id: 'password',
value: '',
label: 'Password',
updatePassword: jest.fn(),
submitFormOnEnterKeyPress: jest.fn(),
classes: {},
};

test('renders password field', () => {
renderWithProviders(<TogglePasswordField {...defaultProps} />);

const passwordInput = screen.getByLabelText('Password');

expect(passwordInput).toBeInTheDocument();
});

test('renders input with type "password"', () => {
renderWithProviders(<TogglePasswordField {...defaultProps} />);

const passwordInput = screen.getByLabelText('Password');

expect(passwordInput).toHaveAttribute('type', 'password');
});

test('toggles password visibility', () => {
renderWithProviders(<TogglePasswordField {...defaultProps} />);

const toggleButton = screen.getByLabelText(
'toggle password visibility',
);
const passwordInput = screen.getByLabelText('Password');

expect(passwordInput).toHaveAttribute('type', 'password');

fireEvent.click(toggleButton);
expect(passwordInput).toHaveAttribute('type', 'text');

fireEvent.click(toggleButton);
expect(passwordInput).toHaveAttribute('type', 'password');
});

test('updates password value', async () => {
renderWithProviders(<TogglePasswordField {...defaultProps} value="newPassword" />);

const passwordInput = screen.getByLabelText('Password');

expect(passwordInput).toHaveValue('newPassword')
});
});
22 changes: 8 additions & 14 deletions src/react/src/components/LoginForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import AppOverflow from './AppOverflow';
import Button from './Button';
import ShowOnly from './ShowOnly';
import SendResetPasswordEmailForm from './SendResetPasswordEmailForm';
import TogglePasswordField from './TogglePasswordField';

import {
updateLoginFormEmailAddress,
Expand Down Expand Up @@ -104,20 +105,13 @@ const LoginForm = ({
}
/>
</div>
<div className="form__field">
<label className="form__label" htmlFor={LOGIN_PASSWORD}>
Password
</label>
<ControlledTextInput
id={LOGIN_PASSWORD}
type="password"
value={password}
onChange={updatePassword}
submitFormOnEnterKeyPress={
submitFormOnEnterKeyPress
}
/>
</div>
<TogglePasswordField
id={LOGIN_PASSWORD}
value={password}
label="Passowrd"
updatePassword={updatePassword}
submitFormOnEnterKeyPress={submitFormOnEnterKeyPress}
/>
<SendResetPasswordEmailForm />
<ShowOnly when={!!(error && error.length)}>
<ul style={formValidationErrorMessageStyle}>
Expand Down
15 changes: 15 additions & 0 deletions src/react/src/components/RegisterFormField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import ControlledTextInput from './ControlledTextInput';
import ControlledSelectInput from './ControlledSelectInput';
import ControlledCheckboxInput from './ControlledCheckboxInput';
import TogglePasswordField from './TogglePasswordField';

import {
inputTypesEnum,
Expand Down Expand Up @@ -72,6 +73,20 @@ export default function RegisterFormField({
);
}

if (type === inputTypesEnum.password) {
return (
<TogglePasswordField
id={id}
value={value}
label={label}
updatePassword={handleChange}
submitFormOnEnterKeyPress={submitFormOnEnterKeyPress}
>
{requiredIndicator}
</TogglePasswordField>
);
}

return (
<div className="form__field">
<label htmlFor={id} className="form__label">
Expand Down
48 changes: 15 additions & 33 deletions src/react/src/components/ResetPasswordForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import Grid from '@material-ui/core/Grid';

import ControlledTextInput from './ControlledTextInput';
import AppGrid from './AppGrid';
import Button from './Button';
import ShowOnly from './ShowOnly';
import TogglePasswordField from './TogglePasswordField';

import {
updateResetPasswordFormUID,
Expand Down Expand Up @@ -75,38 +75,20 @@ class ResetPasswordForm extends Component {
return (
<AppGrid title="Reset Password">
<Grid item xs={12} sm={7}>
<div className="form__field">
<label className="form__label" htmlFor={NEW_PASSWORD}>
New password
</label>
<ControlledTextInput
autoFocus
id={NEW_PASSWORD}
type="password"
value={newPassword}
onChange={updatePassword}
submitFormOnEnterKeyPress={
submitFormOnEnterKeyPress
}
/>
</div>
<div className="form__field">
<label
className="form__label"
htmlFor={CONFIRM_NEW_PASSWORD}
>
Confirm new password
</label>
<ControlledTextInput
id={CONFIRM_NEW_PASSWORD}
type="password"
value={newPasswordConfirmation}
onChange={updateConfirmPassword}
submitFormOnEnterKeyPress={
submitFormOnEnterKeyPress
}
/>
</div>
<TogglePasswordField
id={NEW_PASSWORD}
value={newPassword}
label="New password"
updatePassword={updatePassword}
submitFormOnEnterKeyPress={submitFormOnEnterKeyPress}
/>
<TogglePasswordField
id={CONFIRM_NEW_PASSWORD}
value={newPasswordConfirmation}
label="Confirm new password"
updatePassword={updateConfirmPassword}
submitFormOnEnterKeyPress={submitFormOnEnterKeyPress}
/>
<ShowOnly when={!!(error && error.length)}>
<ul style={formValidationErrorMessageStyle}>
{error && error.length
Expand Down
89 changes: 89 additions & 0 deletions src/react/src/components/TogglePasswordField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useState } from 'react';

import { func, objectOf, string } from 'prop-types';
import { connect } from 'react-redux';
import InputLabel from '@material-ui/core/InputLabel';
import InputBase from '@material-ui/core/InputBase';
import InputAdornment from '@material-ui/core/InputAdornment';
import IconButton from '@material-ui/core/IconButton';
import Visibility from '@material-ui/icons/VisibilityOutlined';
import VisibilityOff from '@material-ui/icons/VisibilityOffOutlined';
import { withStyles } from '@material-ui/core/styles';

import { togglePasswordFieldStyles } from '../util/styles';

function TogglePasswordField({
id,
value,
label,
updatePassword,
submitFormOnEnterKeyPress,
classes,
children,
}) {
const [showPassword, setShowPassword] = useState(false);

const handleClickShowPassword = () => {
setShowPassword(prevShowPassword => !prevShowPassword);
};

const handleMouseDownPassword = event => {
event.preventDefault();
};

return (
<div className="form__field">
<InputLabel className={classes.label} htmlFor={id}>
{label}
{children || null}
</InputLabel>
<InputBase
className={classes.wrapper}
id={id}
type={showPassword ? 'text' : 'password'}
value={value}
onChange={updatePassword}
onKeyPress={submitFormOnEnterKeyPress}
fullWidth
classes={{
inputType: classes.inputType,
input: classes.input,
focused: classes.inputFocused,
}}
endAdornment={
<InputAdornment
position="end"
classes={{
root: classes.adornment,
positionEnd: classes.adornmentPositionEnd,
}}
>
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
classes={{
root: classes.button,
}}
>
{showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
}
/>
</div>
);
}

TogglePasswordField.propTypes = {
id: string.isRequired,
value: string.isRequired,
label: string.isRequired,
updatePassword: func.isRequired,
submitFormOnEnterKeyPress: func.isRequired,
classes: objectOf(string).isRequired,
};

export default connect()(
withStyles(togglePasswordFieldStyles)(TogglePasswordField),
);
18 changes: 18 additions & 0 deletions src/react/src/components/UserProfileField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Checkbox from '@material-ui/core/Checkbox';
import ControlledTextInput from './ControlledTextInput';
import ControlledSelectInput from './ControlledSelectInput';
import ShowOnly from './ShowOnly';
import TogglePasswordField from './TogglePasswordField';

import {
inputTypesEnum,
Expand Down Expand Up @@ -137,6 +138,23 @@ export default function UserProfileField({
);
}

if (type === inputTypesEnum.password) {
return (
<div className="control-panel__group">
<ShowOnly when={!!header}>
<div className="form__field-header">{header}</div>
</ShowOnly>
<TogglePasswordField
id={id}
value={value}
label={label}
updatePassword={handleChange}
submitFormOnEnterKeyPress={submitFormOnEnterKeyPress}
/>
</div>
);
}

return (
<div className="control-panel__group">
<ShowOnly when={!!header}>
Expand Down
40 changes: 40 additions & 0 deletions src/react/src/util/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,46 @@ export const makeFilterStyles = theme =>
}),
});

export const togglePasswordFieldStyles = () =>
Object.freeze({
label: Object.freeze({
marginBottom: '8px',
opacity: 1,
color: '#000000',
fontSize: '14px',
fontWeight: 900,
letterSpacing: '0.5px',
lineHeight: '14px',
textTransform: 'uppercase',
}),
wrapper: Object.freeze({
marginTop: '8px',
boxSizing: 'border-box',
border: '1px solid #D6D8DD',
position: 'relative',
}),
input: Object.freeze({
padding: '8px 56px 11px 16px',
fontSize: '16px',
}),
inputFocused: Object.freeze({
border: '1px solid #3d2f8c',
boxShadow: '0px 0px 8px -1px rgba(12, 70, 225,0.5)',
}),
adornment: Object.freeze({
maxHeight: '2.375em',
position: 'absolute',
top: 0,
right: 0,
}),
adornmentPositionEnd: {
margin: 0,
},
button: Object.freeze({
padding: '8px',
}),
});

export const claimAFacilityFormStyles = Object.freeze({
textFieldStyles: Object.freeze({
width: '95%',
Expand Down

0 comments on commit 0e32d22

Please sign in to comment.