Skip to content

Commit

Permalink
feat(InputField): add show/hide button for password field type (#2006)
Browse files Browse the repository at this point in the history
- toggle button label and icon based on current state
- implement show/hide button to appear after any inputWithin elements
- add tests and snapshots
  • Loading branch information
booc0mtaco authored Jul 5, 2024
1 parent 3eb9077 commit 52d9ca0
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 15 deletions.
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}
{...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

0 comments on commit 52d9ca0

Please sign in to comment.