-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add initial Password Input component (#1449)
- Loading branch information
1 parent
b7b5710
commit 3cc863e
Showing
11 changed files
with
333 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<!-- @license CC0-1.0 --> | ||
|
||
# Password Input | ||
|
||
Helps users enter a password. | ||
|
||
## Guidelines | ||
|
||
- Use this component when the input requires sensitive information, like passwords or PINs. | ||
It ensures that the input is not readable by others who might be looking at the screen. | ||
- The characters entered are hidden, represented by squares. | ||
|
||
This component sets `autocapitalize="none"`, `autocorrect="off"` and `spellcheck="false"` to stop browsers automatically changing user input. | ||
Passwords shouldn’t be checked for spelling or grammar. | ||
This may also prevent posting the password to third-party plugins. | ||
These props cannot be overridden. | ||
|
||
Consider setting the following attributes: | ||
|
||
1. Allow the user’s password manager to automatically fill the password through `autocomplete="current-password"`. | ||
When asking for a new password, use `autocomplete="new-password"` instead. | ||
2. Do not add a `minlength` attribute to ensure passwords meet a minimum length requirement. | ||
This would prematurely indicate an error to the user – while they are still typing. | ||
3. Do not add a `maxlength` attribute either. | ||
Users will not get any feedback when their text input has been truncated, e.g. after pasting from a password manager. | ||
4. If the password is a numeric PIN, add `inputmode="numeric"`. | ||
Devices with virtual keyboards then switch to a numeric keypad layout which makes entering the password easier. | ||
|
||
Follow the [guidelines for asking for passwords](https://design-system.service.gov.uk/patterns/passwords/) of the GOV.UK Design System. |
59 changes: 59 additions & 0 deletions
59
packages/css/src/components/password-input/password-input.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/** | ||
* @license EUPL-1.2+ | ||
* Copyright Gemeente Amsterdam | ||
*/ | ||
|
||
@import "../../common/text-rendering"; | ||
|
||
@mixin reset { | ||
-webkit-appearance: none; // Reset appearance for Safari < 15.4 | ||
appearance: none; // Reset native appearance, this causes issues on iOS and Android devices | ||
border: 0; | ||
border-radius: 0; // Reset rounded borders on iOS devices | ||
box-sizing: border-box; | ||
margin-block: 0; | ||
} | ||
|
||
.ams-password-input { | ||
background-color: var(--ams-password-input-background-color); | ||
box-shadow: var(--ams-password-input-box-shadow); | ||
color: var(--ams-password-input-color); | ||
font-family: var(--ams-password-input-font-family); | ||
font-size: var(--ams-password-input-font-size); | ||
font-weight: var(--ams-password-input-font-weight); | ||
inline-size: 100%; | ||
line-height: var(--ams-password-input-line-height); | ||
outline-offset: var(--ams-password-input-outline-offset); | ||
padding-block: var(--ams-password-input-padding-block); | ||
padding-inline: var(--ams-password-input-padding-inline); | ||
touch-action: manipulation; | ||
|
||
@include text-rendering; | ||
@include reset; | ||
|
||
&:hover { | ||
box-shadow: var(--ams-password-input-hover-box-shadow); | ||
} | ||
} | ||
|
||
.ams-password-input::placeholder { | ||
color: var(--ams-password-input-placeholder-color); | ||
opacity: 100%; // This resets the lower opacity set by Firefox | ||
} | ||
|
||
.ams-password-input:disabled { | ||
background-color: var(--ams-password-input-disabled-background-color); | ||
box-shadow: var(--ams-password-input-disabled-box-shadow); | ||
color: var(--ams-password-input-disabled-color); | ||
cursor: not-allowed; | ||
} | ||
|
||
.ams-password-input:invalid, | ||
.ams-password-input[aria-invalid="true"] { | ||
box-shadow: var(--ams-password-input-invalid-box-shadow); | ||
|
||
&:hover { | ||
// TODO: this should be the (currently non-existent) dark red hover color | ||
box-shadow: var(--ams-password-input-invalid-hover-box-shadow); | ||
} | ||
} |
129 changes: 129 additions & 0 deletions
129
packages/react/src/PasswordInput/PasswordInput.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { render, screen } from '@testing-library/react' | ||
import userEvent from '@testing-library/user-event' | ||
import { createRef, useState } from 'react' | ||
import { PasswordInput } from './PasswordInput' | ||
import { Label } from '../Label' | ||
import '@testing-library/jest-dom' | ||
|
||
describe('Password input', () => { | ||
it('renders', () => { | ||
const { container } = render(<PasswordInput />) | ||
|
||
const component = container.querySelector(':only-child') | ||
|
||
expect(component).toBeInTheDocument() | ||
expect(component).toBeVisible() | ||
}) | ||
|
||
it('renders a design system BEM class name', () => { | ||
const { container } = render(<PasswordInput />) | ||
|
||
const component = container.querySelector(':only-child') | ||
|
||
expect(component).toHaveClass('ams-password-input') | ||
}) | ||
|
||
it('renders an additional class name', () => { | ||
const { container } = render(<PasswordInput className="extra" />) | ||
|
||
const component = container.querySelector(':only-child') | ||
|
||
expect(component).toHaveClass('ams-password-input extra') | ||
}) | ||
|
||
it('renders three attributes for privacy', () => { | ||
const { container } = render(<PasswordInput />) | ||
|
||
const component = container.querySelector(':only-child') | ||
|
||
expect(component).toHaveAttribute('autocapitalize', 'none') | ||
expect(component).toHaveAttribute('autocorrect', 'off') | ||
expect(component).toHaveAttribute('spellcheck', 'false') | ||
}) | ||
|
||
it('should be working in a controlled state', async () => { | ||
function ControlledComponent() { | ||
const [value, setValue] = useState('Hello') | ||
|
||
return <PasswordInput value={value} onChange={(e) => setValue(e.target.value)} /> | ||
} | ||
|
||
const { container } = render(<ControlledComponent />) | ||
|
||
const componentText = screen.getByDisplayValue('Hello') | ||
|
||
expect(componentText).toBeInTheDocument() | ||
|
||
const component = container.querySelector(':only-child') | ||
if (component) { | ||
await userEvent.type(component, ', World!') | ||
} | ||
|
||
const newComponentText = screen.getByDisplayValue('Hello, World!') | ||
|
||
expect(newComponentText).toBeInTheDocument() | ||
}) | ||
|
||
it('should not update the value when disabled', async () => { | ||
const { container } = render(<PasswordInput disabled defaultValue="Hello" />) | ||
|
||
const component = container.querySelector(':only-child') | ||
if (component) { | ||
await userEvent.type(component, ', World!') | ||
} | ||
|
||
expect(component).toHaveValue('Hello') | ||
}) | ||
|
||
it('supports ForwardRef in React', () => { | ||
const ref = createRef<HTMLInputElement>() | ||
|
||
const { container } = render(<PasswordInput ref={ref} />) | ||
|
||
const component = container.querySelector(':only-child') | ||
|
||
expect(ref.current).toBe(component) | ||
}) | ||
|
||
describe('Invalid state', () => { | ||
it('is not invalid by default', () => { | ||
const { container } = render(<PasswordInput />) | ||
|
||
const component = container.querySelector(':only-child') | ||
|
||
expect(component).not.toBeInvalid() | ||
}) | ||
|
||
it('can have an invalid state', () => { | ||
const { container } = render(<PasswordInput invalid />) | ||
|
||
const component = container.querySelector(':only-child') | ||
|
||
expect(component).toHaveAttribute('aria-invalid') | ||
expect(component).toBeInvalid() | ||
}) | ||
|
||
it('omits non-essential invalid attributes when not invalid', () => { | ||
const { container } = render(<PasswordInput invalid={false} />) | ||
|
||
const component = container.querySelector(':only-child') | ||
|
||
expect(component).not.toHaveAttribute('aria-invalid') | ||
}) | ||
}) | ||
|
||
describe('Type', () => { | ||
it('sets the ‘password’ type', () => { | ||
render( | ||
<> | ||
<Label htmlFor="password-field">Password</Label> | ||
<PasswordInput id="password-field" /> | ||
</>, | ||
) | ||
|
||
const component = screen.getByLabelText(/password/i) | ||
|
||
expect(component).toHaveAttribute('type', 'password') | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/** | ||
* @license EUPL-1.2+ | ||
* Copyright Gemeente Amsterdam | ||
*/ | ||
|
||
import clsx from 'clsx' | ||
import { forwardRef } from 'react' | ||
import type { ForwardedRef, InputHTMLAttributes } from 'react' | ||
|
||
export type PasswordInputProps = { | ||
/** Whether the value fails a validation rule. */ | ||
invalid?: boolean | ||
} & Omit< | ||
InputHTMLAttributes<HTMLInputElement>, | ||
'aria-invalid' | 'autoCapitalize' | 'autoCorrect' | 'spellCheck' | 'type' | ||
> | ||
|
||
export const PasswordInput = forwardRef( | ||
({ className, dir, invalid, ...restProps }: PasswordInputProps, ref: ForwardedRef<HTMLInputElement>) => ( | ||
<input | ||
{...restProps} | ||
aria-invalid={invalid || undefined} | ||
autoCapitalize="none" | ||
autoCorrect="off" | ||
className={clsx('ams-password-input', className)} | ||
dir={dir ?? 'auto'} | ||
ref={ref} | ||
spellCheck="false" | ||
type="password" | ||
/> | ||
), | ||
) | ||
|
||
PasswordInput.displayName = 'PasswordInput' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<!-- @license CC0-1.0 --> | ||
|
||
# React Password Input component | ||
|
||
[Password Input documentation](../../../css/src/components/password-input/README.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { PasswordInput } from './PasswordInput' | ||
export type { PasswordInputProps } from './PasswordInput' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
proprietary/tokens/src/components/ams/password-input.tokens.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"ams": { | ||
"password-input": { | ||
"background-color": { "value": "{ams.color.primary-white}" }, | ||
"box-shadow": { "value": "inset 0 0 0 {ams.border.width.sm} {ams.color.primary-black}" }, | ||
"color": { "value": "{ams.color.primary-black}" }, | ||
"font-family": { "value": "{ams.text.font-family}" }, | ||
"font-size": { "value": "{ams.text.level.5.font-size}" }, | ||
"font-weight": { "value": "{ams.text.font-weight.normal}" }, | ||
"line-height": { "value": "{ams.text.level.5.line-height}" }, | ||
"outline-offset": { "value": "{ams.focus.outline-offset}" }, | ||
"padding-block": { "value": "{ams.space.sm}" }, | ||
"padding-inline": { "value": "{ams.space.md}" }, | ||
"disabled": { | ||
"background-color": { "value": "{ams.color.primary-white}" }, | ||
"box-shadow": { "value": "inset 0 0 0 {ams.border.width.sm} {ams.color.neutral-grey2}" }, | ||
"color": { "value": "{ams.color.neutral-grey2}" } | ||
}, | ||
"hover": { | ||
"box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.primary-black}" } | ||
}, | ||
"invalid": { | ||
"box-shadow": { "value": "inset 0 0 0 {ams.border.width.sm} {ams.color.primary-red}" }, | ||
"hover": { | ||
"box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.primary-red}" } | ||
} | ||
}, | ||
"placeholder": { | ||
"color": { "value": "{ams.color.neutral-grey3}" } | ||
} | ||
} | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
storybook/src/components/PasswordInput/PasswordInput.docs.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{/* @license CC0-1.0 */} | ||
|
||
import { Controls, Markdown, Meta, Primary } from "@storybook/blocks"; | ||
import * as PasswordInputStories from "./PasswordInput.stories.tsx"; | ||
import README from "../../../../packages/css/src/components/password-input/README.md?raw"; | ||
|
||
<Meta of={PasswordInputStories} /> | ||
|
||
<Markdown>{README}</Markdown> | ||
|
||
<Primary /> | ||
|
||
<Controls /> |
27 changes: 27 additions & 0 deletions
27
storybook/src/components/PasswordInput/PasswordInput.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** | ||
* @license EUPL-1.2+ | ||
* Copyright Gemeente Amsterdam | ||
*/ | ||
|
||
import { PasswordInput } from '@amsterdam/design-system-react/src' | ||
import { Meta, StoryObj } from '@storybook/react' | ||
|
||
const meta = { | ||
title: 'Components/Forms/Password Input', | ||
component: PasswordInput, | ||
args: { | ||
disabled: false, | ||
invalid: false, | ||
}, | ||
argTypes: { | ||
disabled: { | ||
description: 'Prevents interaction. Avoid if possible.', | ||
}, | ||
}, | ||
} satisfies Meta<typeof PasswordInput> | ||
|
||
export default meta | ||
|
||
type Story = StoryObj<typeof meta> | ||
|
||
export const Default: Story = {} |