Skip to content

Commit 9f420c5

Browse files
jonnybelmontezume
authored andcommitted
feat(fields/password): add show/hide button (#555)
* feat(icons): add new Eye icon * feat(fields/password): add show/hide button to PasswordField
1 parent 733408f commit 9f420c5

File tree

11 files changed

+98
-15
lines changed

11 files changed

+98
-15
lines changed

i18n/core.json

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"UIKit.MoneyField.highPrecision": "High Precision Price",
2929
"UIKit.MultilineTextInput.collapse": "Collapse",
3030
"UIKit.MultilineTextInput.expand": "Expand",
31+
"UIKit.PasswordField.hide": "hide",
32+
"UIKit.PasswordField.show": "show",
3133
"UIKit.SelectInput.noOptionsMessageWithInputValue": "No options",
3234
"UIKit.SelectInput.noOptionsMessageWithoutInputValue": "No options",
3335
"UIKit.TimeInput.placeholder": "HH:mm AM/PM"

i18n/de.json

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"UIKit.MoneyField.highPrecision": "",
2929
"UIKit.MultilineTextInput.collapse": "Ausblenden",
3030
"UIKit.MultilineTextInput.expand": "Erweitern",
31+
"UIKit.PasswordField.hide": "verbergen",
32+
"UIKit.PasswordField.show": "anzeigen",
3133
"UIKit.SelectInput.noOptionsMessageWithInputValue": "",
3234
"UIKit.SelectInput.noOptionsMessageWithoutInputValue": "",
3335
"UIKit.TimeInput.placeholder": "HH:mm"

i18n/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"UIKit.MoneyField.highPrecision": "High Precision Price",
2929
"UIKit.MultilineTextInput.collapse": "Collapse",
3030
"UIKit.MultilineTextInput.expand": "Expand",
31+
"UIKit.PasswordField.hide": "hide",
32+
"UIKit.PasswordField.show": "show",
3133
"UIKit.SelectInput.noOptionsMessageWithInputValue": "No options",
3234
"UIKit.SelectInput.noOptionsMessageWithoutInputValue": "No options",
3335
"UIKit.TimeInput.placeholder": "HH:mm AM/PM"

i18n/es.json

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"UIKit.MoneyField.highPrecision": "",
2929
"UIKit.MultilineTextInput.collapse": "Contraer",
3030
"UIKit.MultilineTextInput.expand": "Expandir",
31+
"UIKit.PasswordField.hide": "ocultar",
32+
"UIKit.PasswordField.show": "mostrar",
3133
"UIKit.SelectInput.noOptionsMessageWithInputValue": "Menú vacío",
3234
"UIKit.SelectInput.noOptionsMessageWithoutInputValue": "Menú vacío",
3335
"UIKit.TimeInput.placeholder": "HH:mm"

src/components/fields/password-field/README.md

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import { PasswordField } from '@commercetools-frontend/ui-kit';
3333
| `onBlur` | `func` | - | - | - | Called when input is blurred |
3434
| `onFocus` | `func` | - | - | - | Called when input is focused |
3535
| `isAutofocussed` | `bool` | - | - | - | Focus the input on initial render |
36-
| `isPasswordVisible` | `bool` | - | - | `false` | Indicates whether we show the password or not | |
3736
| `isDisabled` | `bool` | - | - | `false` | Indicates that the input cannot be modified (e.g not authorised, or changes currently saving). |
3837
| `isReadOnly` | `bool` | - | - | `false` | Indicates that the field is displaying read-only content |
3938
| `placeholder` | `string` | - | - | - | Placeholder text for the input |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineMessages } from 'react-intl';
2+
3+
export default defineMessages({
4+
show: {
5+
id: 'UIKit.PasswordField.show',
6+
description: 'Label for the button to show the password',
7+
defaultMessage: 'show',
8+
},
9+
hide: {
10+
id: 'UIKit.PasswordField.hide',
11+
description: 'Label for the button to hide the password',
12+
defaultMessage: 'hide',
13+
},
14+
});

src/components/fields/password-field/password-field.js

+44-13
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3+
import { injectIntl } from 'react-intl';
34
import requiredIf from 'react-required-if';
45
import Constraints from '../../constraints';
56
import Spacings from '../../spacings';
67
import FieldLabel from '../../field-label';
78
import PasswordInput from '../../inputs/password-input';
9+
import FlatButton from '../../buttons/flat-button';
10+
import { EyeIcon, EyeCrossedIcon } from '../../icons';
811
import getFieldId from '../../../utils/get-field-id';
912
import createSequentialId from '../../../utils/create-sequential-id';
1013
import FieldErrors from '../../field-errors';
1114
import filterDataAttributes from '../../../utils/filter-data-attributes';
15+
import messages from './messages';
1216

1317
const sequentialId = createSequentialId('password-field-');
1418

@@ -27,6 +31,9 @@ class PasswordField extends React.Component {
2731
renderError: PropTypes.func,
2832
isRequired: PropTypes.bool,
2933
touched: PropTypes.bool,
34+
intl: PropTypes.shape({
35+
formatMessage: PropTypes.func.isRequired,
36+
}).isRequired,
3037

3138
// PasswordInput
3239
name: PropTypes.string,
@@ -35,7 +42,6 @@ class PasswordField extends React.Component {
3542
onBlur: PropTypes.func,
3643
onFocus: PropTypes.func,
3744
isAutofocussed: PropTypes.bool,
38-
isPasswordVisible: PropTypes.bool,
3945
isDisabled: PropTypes.bool,
4046
isReadOnly: PropTypes.bool,
4147
placeholder: PropTypes.string,
@@ -61,6 +67,7 @@ class PasswordField extends React.Component {
6167
// We generate an id in case no id is provided by the parent to attach the
6268
// label to the input component.
6369
id: this.props.id,
70+
isPasswordVisible: false,
6471
};
6572

6673
static getDerivedStateFromProps = (props, state) => ({
@@ -72,16 +79,40 @@ class PasswordField extends React.Component {
7279
return (
7380
<Constraints.Horizontal constraint={this.props.horizontalConstraint}>
7481
<Spacings.Stack scale="xs">
75-
<FieldLabel
76-
title={this.props.title}
77-
hint={this.props.hint}
78-
description={this.props.description}
79-
onInfoButtonClick={this.props.onInfoButtonClick}
80-
hintIcon={this.props.hintIcon}
81-
badge={this.props.badge}
82-
hasRequiredIndicator={this.props.isRequired}
83-
htmlFor={this.state.id}
84-
/>
82+
<Spacings.Inline alignItems="center" justifyContent="space-between">
83+
<FieldLabel
84+
hint={this.props.hint}
85+
title={this.props.title}
86+
badge={this.props.badge}
87+
htmlFor={this.state.id}
88+
hintIcon={this.props.hintIcon}
89+
description={this.props.description}
90+
onInfoButtonClick={this.props.onInfoButtonClick}
91+
hasRequiredIndicator={this.props.isRequired}
92+
/>
93+
{!this.props.isDisabled && !this.props.isReadOnly && (
94+
<FlatButton
95+
icon={
96+
this.state.isPasswordVisible ? (
97+
<EyeCrossedIcon />
98+
) : (
99+
<EyeIcon />
100+
)
101+
}
102+
label={
103+
this.state.isPasswordVisible
104+
? this.props.intl.formatMessage(messages.hide)
105+
: this.props.intl.formatMessage(messages.show)
106+
}
107+
onClick={() =>
108+
this.setState(prevState => ({
109+
isPasswordVisible: !prevState.isPasswordVisible,
110+
}))
111+
}
112+
isDisabled={!this.props.value}
113+
/>
114+
)}
115+
</Spacings.Inline>
85116
<PasswordInput
86117
id={this.state.id}
87118
name={this.props.name}
@@ -90,7 +121,7 @@ class PasswordField extends React.Component {
90121
onBlur={this.props.onBlur}
91122
onFocus={this.props.onFocus}
92123
isAutofocussed={this.props.isAutofocussed}
93-
isPasswordVisible={this.props.isPasswordVisible}
124+
isPasswordVisible={this.state.isPasswordVisible}
94125
isDisabled={this.props.isDisabled}
95126
isReadOnly={this.props.isReadOnly}
96127
hasError={hasError}
@@ -110,4 +141,4 @@ class PasswordField extends React.Component {
110141
}
111142
}
112143

113-
export default PasswordField;
144+
export default injectIntl(PasswordField);

src/components/fields/password-field/password-field.spec.js

+21
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,24 @@ describe('when field is touched and has errors', () => {
191191
});
192192
});
193193
});
194+
195+
describe('when input has no value', () => {
196+
it('should disable the `show` button`', () => {
197+
const { getByLabelText } = renderPasswordField();
198+
expect(getByLabelText('show')).toHaveAttribute('disabled');
199+
});
200+
});
201+
202+
describe('when input value is not empty', () => {
203+
it('should enable the `show` button`', () => {
204+
const { getByLabelText } = renderPasswordField({ value: 'foo' });
205+
expect(getByLabelText('show')).not.toHaveAttribute('disabled');
206+
});
207+
describe('when the `show` button is clicked', () => {
208+
it('should change the label of the button to `hide`', () => {
209+
const { getByLabelText } = renderPasswordField({ value: 'foo' });
210+
getByLabelText('show').click();
211+
expect(getByLabelText('hide')).toBeInTheDocument();
212+
});
213+
});
214+
});

src/components/fields/password-field/password-field.story.js

-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ storiesOf('Components|Fields', module)
4848
}
4949
}}
5050
isRequired={boolean('isRequired', false)}
51-
isPasswordVisible={boolean('isPasswordVisible', false)}
5251
touched={boolean('touched', false)}
5352
name={text('name', '')}
5453
value={text('value', value)}

src/components/icons/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import OrigExpandIcon from './svg/expand.react.svg';
5656
import OrigExportIcon from './svg/export.react.svg';
5757
import OrigExternalLinkIcon from './svg/external-link.react.svg';
5858
import OrigEyeCrossedIcon from './svg/eye-crossed.react.svg';
59+
import OrigEyeIcon from './svg/eye.react.svg';
5960
import OrigFilterIcon from './svg/filter.react.svg';
6061
import OrigFlagFilledIcon from './svg/flag-filled.react.svg';
6162
import OrigFlagLinearIcon from './svg/flag-linear.react.svg';
@@ -243,6 +244,7 @@ export const EyeCrossedIcon = createStyledIcon(
243244
OrigEyeCrossedIcon,
244245
'EyeCrossedIcon'
245246
);
247+
export const EyeIcon = createStyledIcon(OrigEyeIcon, 'EyeIcon');
246248
export const FilterIcon = createStyledIcon(OrigFilterIcon, 'FilterIcon');
247249
export const FlagFilledIcon = createStyledIcon(
248250
OrigFlagFilledIcon,
+9
Loading

0 commit comments

Comments
 (0)