Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(InputField): add show/hide button for password field type #2006

Merged
merged 1 commit into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion src/components/InputField/InputField.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { StoryObj, Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import React from 'react';

import { InputField } from './InputField';
Expand All @@ -19,6 +20,26 @@ const meta: Meta<typeof InputField> = {
args: {
className: 'w-96',
},
argTypes: {
type: {
control: 'select',
options: [
'text',
'password',
'datetime',
'datetime-local',
'date',
'month',
'time',
'week',
'number',
'email',
'url',
'search',
'tel',
],
},
},
decorators: [(Story) => <div className="p-8">{Story()}</div>],
};

Expand Down Expand Up @@ -147,12 +168,29 @@ export const NoVisibleLabel: Story = {
};

/**
* Password fields show dots instead of characters, to help with security.
* Password fields show dots instead of characters, to help with security. They allow for show/hide of the field
* contents.
*/
export const Password: Story = {
args: {
label: 'Password',
type: 'password',
defaultValue: 'secret123',
},
};

/**
* Password fields show dots instead of characters, to help with security. They allow for show/hide of the field
* contents, and resetting.
*/
export const PasswordWithShownText: Story = {
args: {
...Password.args,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const showHideButton = await canvas.findByRole('button');
await userEvent.click(showHideButton);
},
};

Expand Down
21 changes: 19 additions & 2 deletions src/components/InputField/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ForwardedRefComponent,
} from '../../util/utility-types';
import type { Status } from '../../util/variant-types';
import Button from '../Button';
import FieldLabel from '../FieldLabel';
import FieldNote from '../FieldNote';
import Icon, { type IconName } from '../Icon';
Expand Down Expand Up @@ -180,6 +181,10 @@ export const InputField: InputFieldType = forwardRef(
const shouldRenderOverline = !!(label || required);
const [fieldText, setFieldText] = useState(other.defaultValue);

// Handling of behavior when field type is password. Show/hide button
const revealShowHideButton = type === 'password';
const [isPasswordVisible, setIsPasswordVisible] = useState(false);

const overlineClassName = clsx(
styles['input-field__overline'],
!label && styles['input-field__overline--no-label'],
Expand Down Expand Up @@ -292,12 +297,24 @@ export const InputField: InputFieldType = forwardRef(
ref={ref}
required={required}
status={shouldRenderError ? 'critical' : status}
type={type}
type={isPasswordVisible ? 'text' : type}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not having the type="password" may mess with the usage of autocomplete, but since we're using Google Login for Render I don't think this is an issue

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/password#allowing_autocomplete

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, and there are a few safeguards for the scenario:

  • this switching to text only applies after interacting with the button, so by default it's a regular password field
  • I tested locally and I get the 1Password prompt (which nicely overlays the show/hide button) when set to password

My gut on the normal usage here is that a person would usually either paste into the password field, use the UI (when still set to type "password"), or reveal the text IFF they want to type it manually and see what they are typing/pasting or verify it after the fact.

{...other}
/>
{inputWithin && (
{(inputWithin || type === 'password') && (
<div className={styles['input-field__input-within']}>
{inputWithin}
{revealShowHideButton && fieldText && (
<Button
aria-label={`${isPasswordVisible ? 'Hide' : 'Show'} password`}
icon={isPasswordVisible ? 'eye-closed' : 'eye-open'}
iconLayout="icon-only"
onClick={() => {
setIsPasswordVisible(!isPasswordVisible);
}}
rank="tertiary"
size="md"
/>
)}
</div>
)}
{leadingIcon && (
Expand Down
113 changes: 101 additions & 12 deletions src/components/InputField/__snapshots__/InputField.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ exports[`<InputField /> InputWithin story renders snapshot 1`] = `
>
<label
class="label label--lg input-field__label"
for=":rq:"
for=":rs:"
>
Input field with button inside
</label>
Expand All @@ -156,7 +156,7 @@ exports[`<InputField /> InputWithin story renders snapshot 1`] = `
<input
aria-invalid="false"
class="input input-field__input--input-within"
id=":rq:"
id=":rs:"
type="text"
/>
<div
Expand Down Expand Up @@ -308,7 +308,96 @@ exports[`<InputField /> Password story renders snapshot 1`] = `
class="input"
id=":rm:"
type="password"
value="secret123"
/>
<div
class="input-field__input-within"
>
<button
aria-label="Show password"
class="button button--layout-icon-only button--tertiary button--md button--variant-default"
type="button"
>
<span
class="button__text"
>
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
height="1rem"
style="--icon-size: 1rem;"
viewBox="0 0 24 24"
width="1rem"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 4C7 4 2.73 7.11 1 11.5C2.73 15.89 7 19 12 19C17 19 21.27 15.89 23 11.5C21.27 7.11 17 4 12 4ZM12 16.5C9.24 16.5 7 14.26 7 11.5C7 8.74 9.24 6.5 12 6.5C14.76 6.5 17 8.74 17 11.5C17 14.26 14.76 16.5 12 16.5ZM12 8.5C10.34 8.5 9 9.84 9 11.5C9 13.16 10.34 14.5 12 14.5C13.66 14.5 15 13.16 15 11.5C15 9.84 13.66 8.5 12 8.5Z"
/>
</svg>
</span>
</button>
</div>
</div>
</div>
</div>
`;

exports[`<InputField /> PasswordWithShownText story renders snapshot 1`] = `
<div
class="p-8"
>
<div
class="w-96"
>
<div
class="input-field__overline"
>
<label
class="label label--lg input-field__label"
for=":ro:"
>
Password
</label>
</div>
<div
class="input-field__body"
>
<input
aria-invalid="false"
class="input"
id=":ro:"
type="text"
value="secret123"
/>
<div
class="input-field__input-within"
>
<button
aria-label="Hide password"
class="button button--layout-icon-only button--tertiary button--md button--variant-default"
type="button"
>
<span
class="button__text"
>
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
height="1rem"
style="--icon-size: 1rem;"
viewBox="0 0 24 24"
width="1rem"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 6.49969C14.76 6.49969 17 8.73969 17 11.4997C17 12.0097 16.9 12.4997 16.76 12.9597L19.82 16.0197C21.21 14.7897 22.31 13.2497 23 11.4897C21.27 7.10969 17 3.99969 12 3.99969C10.73 3.99969 9.51 4.19969 8.36 4.56969L10.53 6.73969C11 6.59969 11.49 6.49969 12 6.49969ZM2.71 3.15969C2.32 3.54969 2.32 4.17969 2.71 4.56969L4.68 6.53969C3.06 7.82969 1.77 9.52969 1 11.4997C2.73 15.8897 7 18.9997 12 18.9997C13.52 18.9997 14.97 18.6997 16.31 18.1797L19.03 20.8997C19.42 21.2897 20.05 21.2897 20.44 20.8997C20.83 20.5097 20.83 19.8797 20.44 19.4897L4.13 3.15969C3.74 2.76969 3.1 2.76969 2.71 3.15969ZM12 16.4997C9.24 16.4997 7 14.2597 7 11.4997C7 10.7297 7.18 9.99969 7.49 9.35969L9.06 10.9297C9.03 11.1097 9 11.2997 9 11.4997C9 13.1597 10.34 14.4997 12 14.4997C12.2 14.4997 12.38 14.4697 12.57 14.4297L14.14 15.9997C13.49 16.3197 12.77 16.4997 12 16.4997ZM14.97 11.1697C14.82 9.76969 13.72 8.67969 12.33 8.52969L14.97 11.1697Z"
/>
</svg>
</span>
</button>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -458,7 +547,7 @@ exports[`<InputField /> ShowHint story renders snapshot 1`] = `
>
<label
class="label label--lg input-field__label"
for=":ro:"
for=":rq:"
>
Field with Optional Hint
</label>
Expand All @@ -474,7 +563,7 @@ exports[`<InputField /> ShowHint story renders snapshot 1`] = `
<input
aria-invalid="false"
class="input"
id=":ro:"
id=":rq:"
type="text"
/>
</div>
Expand Down Expand Up @@ -621,7 +710,7 @@ exports[`<InputField /> WithAMaxLength story renders snapshot 1`] = `
>
<label
class="label label--lg input-field__label"
for=":rs:"
for=":ru:"
>
test label
</label>
Expand All @@ -644,7 +733,7 @@ exports[`<InputField /> WithAMaxLength story renders snapshot 1`] = `
<input
aria-invalid="false"
class="input error"
id=":rs:"
id=":ru:"
maxlength="15"
required=""
type="text"
Expand All @@ -670,7 +759,7 @@ exports[`<InputField /> WithARecommendedLength story renders snapshot 1`] = `
>
<label
class="label label--lg input-field__label"
for=":ru:"
for=":r10:"
>
Shortened Length Field
</label>
Expand All @@ -693,7 +782,7 @@ exports[`<InputField /> WithARecommendedLength story renders snapshot 1`] = `
<input
aria-invalid="false"
class="input error"
id=":ru:"
id=":r10:"
required=""
type="text"
value="Some initial text"
Expand All @@ -718,7 +807,7 @@ exports[`<InputField /> WithBothMaxAndRecommendedLength story renders snapshot 1
>
<label
class="label label--lg input-field__label"
for=":r10:"
for=":r12:"
>
test label
</label>
Expand All @@ -739,10 +828,10 @@ exports[`<InputField /> WithBothMaxAndRecommendedLength story renders snapshot 1
class="input-field__body input-field--has-fieldNote"
>
<input
aria-describedby=":r11:"
aria-describedby=":r13:"
aria-invalid="false"
class="input error"
id=":r10:"
id=":r12:"
maxlength="20"
required=""
type="text"
Expand All @@ -754,7 +843,7 @@ exports[`<InputField /> WithBothMaxAndRecommendedLength story renders snapshot 1
>
<div
class="field-note field-note--error"
id=":r11:"
id=":r13:"
>
<svg
class="icon field-note__icon"
Expand Down
Loading