From 9a417cacfd00b869e6b72f458796f7b88b12834b Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Mon, 22 Jul 2024 22:57:42 +0200 Subject: [PATCH 01/13] Scaffold new Password Input component --- packages/css/src/components/index.scss | 1 + .../src/components/password-input/README.md | 3 ++ .../password-input/password-input.scss | 8 ++++ .../src/PasswordInput/PasswordInput.test.tsx | 41 +++++++++++++++++++ .../react/src/PasswordInput/PasswordInput.tsx | 20 +++++++++ packages/react/src/PasswordInput/README.md | 5 +++ packages/react/src/PasswordInput/index.ts | 2 + packages/react/src/index.ts | 1 + .../components/ams/password-input.tokens.json | 5 +++ .../PasswordInput/PasswordInput.docs.mdx | 13 ++++++ .../PasswordInput/PasswordInput.stories.tsx | 21 ++++++++++ 11 files changed, 120 insertions(+) create mode 100644 packages/css/src/components/password-input/README.md create mode 100644 packages/css/src/components/password-input/password-input.scss create mode 100644 packages/react/src/PasswordInput/PasswordInput.test.tsx create mode 100644 packages/react/src/PasswordInput/PasswordInput.tsx create mode 100644 packages/react/src/PasswordInput/README.md create mode 100644 packages/react/src/PasswordInput/index.ts create mode 100644 proprietary/tokens/src/components/ams/password-input.tokens.json create mode 100644 storybook/src/components/PasswordInput/PasswordInput.docs.mdx create mode 100644 storybook/src/components/PasswordInput/PasswordInput.stories.tsx diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss index de2ceed869..0b5b2636bf 100644 --- a/packages/css/src/components/index.scss +++ b/packages/css/src/components/index.scss @@ -4,6 +4,7 @@ */ /* Append here */ +@import "./password-input/password-input"; @import "./form-error-list/form-error-list"; @import "./table-of-contents/table-of-contents"; @import "./error-message/error-message"; diff --git a/packages/css/src/components/password-input/README.md b/packages/css/src/components/password-input/README.md new file mode 100644 index 0000000000..9c6d64a04a --- /dev/null +++ b/packages/css/src/components/password-input/README.md @@ -0,0 +1,3 @@ + + +# Password Input diff --git a/packages/css/src/components/password-input/password-input.scss b/packages/css/src/components/password-input/password-input.scss new file mode 100644 index 0000000000..d6693d2515 --- /dev/null +++ b/packages/css/src/components/password-input/password-input.scss @@ -0,0 +1,8 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +.ams-password-input { + /* Add styles here */ +} diff --git a/packages/react/src/PasswordInput/PasswordInput.test.tsx b/packages/react/src/PasswordInput/PasswordInput.test.tsx new file mode 100644 index 0000000000..7cf984f815 --- /dev/null +++ b/packages/react/src/PasswordInput/PasswordInput.test.tsx @@ -0,0 +1,41 @@ +import { render } from '@testing-library/react' +import { createRef } from 'react' +import { PasswordInput } from './PasswordInput' +import '@testing-library/jest-dom' + +describe('Password input', () => { + it('renders', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toBeInTheDocument() + expect(component).toBeVisible() + }) + + it('renders a design system BEM class name', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toHaveClass('ams-password-input') + }) + + it('renders an additional class name', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toHaveClass('ams-password-input extra') + }) + + it('supports ForwardRef in React', () => { + const ref = createRef() + + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(ref.current).toBe(component) + }) +}) diff --git a/packages/react/src/PasswordInput/PasswordInput.tsx b/packages/react/src/PasswordInput/PasswordInput.tsx new file mode 100644 index 0000000000..a44b0ee028 --- /dev/null +++ b/packages/react/src/PasswordInput/PasswordInput.tsx @@ -0,0 +1,20 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import clsx from 'clsx' +import { forwardRef } from 'react' +import type { ForwardedRef, InputHTMLAttributes, PropsWithChildren } from 'react' + +export type PasswordInputProps = PropsWithChildren> + +export const PasswordInput = forwardRef( + ({ children, className, ...restProps }: PasswordInputProps, ref: ForwardedRef) => ( + + {children} + + ), +) + +PasswordInput.displayName = 'PasswordInput' diff --git a/packages/react/src/PasswordInput/README.md b/packages/react/src/PasswordInput/README.md new file mode 100644 index 0000000000..725d78502d --- /dev/null +++ b/packages/react/src/PasswordInput/README.md @@ -0,0 +1,5 @@ + + +# React Password Input component + +[Password Input documentation](../../../css/src/components/password-input/README.md) diff --git a/packages/react/src/PasswordInput/index.ts b/packages/react/src/PasswordInput/index.ts new file mode 100644 index 0000000000..1d4d6d8a40 --- /dev/null +++ b/packages/react/src/PasswordInput/index.ts @@ -0,0 +1,2 @@ +export { PasswordInput } from './PasswordInput' +export type { PasswordInputProps } from './PasswordInput' diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index d1e4be314f..f616032c4e 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -4,6 +4,7 @@ */ /* Append here */ +export * from './PasswordInput' export * from './FormErrorList' export * from './TableOfContents' export * from './ErrorMessage' diff --git a/proprietary/tokens/src/components/ams/password-input.tokens.json b/proprietary/tokens/src/components/ams/password-input.tokens.json new file mode 100644 index 0000000000..7123df8c81 --- /dev/null +++ b/proprietary/tokens/src/components/ams/password-input.tokens.json @@ -0,0 +1,5 @@ +{ + "ams": { + "password-input": {} + } +} diff --git a/storybook/src/components/PasswordInput/PasswordInput.docs.mdx b/storybook/src/components/PasswordInput/PasswordInput.docs.mdx new file mode 100644 index 0000000000..b06271db43 --- /dev/null +++ b/storybook/src/components/PasswordInput/PasswordInput.docs.mdx @@ -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"; + + + +{README} + + + + diff --git a/storybook/src/components/PasswordInput/PasswordInput.stories.tsx b/storybook/src/components/PasswordInput/PasswordInput.stories.tsx new file mode 100644 index 0000000000..f7859dab9e --- /dev/null +++ b/storybook/src/components/PasswordInput/PasswordInput.stories.tsx @@ -0,0 +1,21 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import { PasswordInput } from '@amsterdam/design-system-react/src' +import { Meta, StoryObj } from '@storybook/react' + +const meta = { + title: 'Password Input', + component: PasswordInput, + args: { + children: 'Nieuw component', + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = {} From c80dabab0982f19c811f11606e0c98fbab78198c Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Mon, 22 Jul 2024 23:59:35 +0200 Subject: [PATCH 02/13] Implement MVP Password Input component --- .../src/components/password-input/README.md | 22 ++++++++ .../password-input/password-input.scss | 53 ++++++++++++++++++- .../src/PasswordInput/PasswordInput.test.tsx | 45 +++++++++++++++- .../react/src/PasswordInput/PasswordInput.tsx | 19 ++++--- .../components/ams/password-input.tokens.json | 27 +++++++++- .../PasswordInput/PasswordInput.stories.tsx | 11 +++- 6 files changed, 166 insertions(+), 11 deletions(-) diff --git a/packages/css/src/components/password-input/README.md b/packages/css/src/components/password-input/README.md index 9c6d64a04a..1775c4d128 100644 --- a/packages/css/src/components/password-input/README.md +++ b/packages/css/src/components/password-input/README.md @@ -1,3 +1,25 @@ # Password Input + +Helps users enter a password. + +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. + +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. Add a `minlength` attribute to ensure passwords meet a minimum length requirement. + Do not add a `maxlength` attribute. +3. Use the `pattern` attribute to enforce password policies, like including numbers and special characters. + Describe these policies in the [Field](?path=/docs/components-forms-field--docs)’s description as well. +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. +5. Set `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. + +Follow the [guidelines for asking for passwords](https://design-system.service.gov.uk/patterns/passwords/) of the GOV.UK Design System. diff --git a/packages/css/src/components/password-input/password-input.scss b/packages/css/src/components/password-input/password-input.scss index d6693d2515..108bfe395f 100644 --- a/packages/css/src/components/password-input/password-input.scss +++ b/packages/css/src/components/password-input/password-input.scss @@ -3,6 +3,57 @@ * 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 { - /* Add styles here */ + 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-text-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); + } } diff --git a/packages/react/src/PasswordInput/PasswordInput.test.tsx b/packages/react/src/PasswordInput/PasswordInput.test.tsx index 7cf984f815..08fdb73dc1 100644 --- a/packages/react/src/PasswordInput/PasswordInput.test.tsx +++ b/packages/react/src/PasswordInput/PasswordInput.test.tsx @@ -1,6 +1,7 @@ -import { render } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { createRef } from 'react' import { PasswordInput } from './PasswordInput' +import { Label } from '../Label' import '@testing-library/jest-dom' describe('Password input', () => { @@ -38,4 +39,46 @@ describe('Password input', () => { expect(ref.current).toBe(component) }) + + describe('Invalid state', () => { + it('is not invalid by default', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).not.toBeInvalid() + }) + + it('can have an invalid state', () => { + const { container } = render() + + 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() + + const component = container.querySelector(':only-child') + + expect(component).not.toHaveAttribute('aria-invalid') + }) + }) + + describe('Type', () => { + it('sets the ‘password’ type', () => { + render( + <> + + + , + ) + + const component = screen.getByLabelText(/password/i) + + expect(component).toHaveAttribute('type', 'password') + }) + }) }) diff --git a/packages/react/src/PasswordInput/PasswordInput.tsx b/packages/react/src/PasswordInput/PasswordInput.tsx index a44b0ee028..422dcad461 100644 --- a/packages/react/src/PasswordInput/PasswordInput.tsx +++ b/packages/react/src/PasswordInput/PasswordInput.tsx @@ -5,15 +5,22 @@ import clsx from 'clsx' import { forwardRef } from 'react' -import type { ForwardedRef, InputHTMLAttributes, PropsWithChildren } from 'react' +import type { ForwardedRef, InputHTMLAttributes } from 'react' -export type PasswordInputProps = PropsWithChildren> +export type PasswordInputProps = { + /** Whether the value fails a validation rule. */ + invalid?: boolean +} & Omit, 'aria-invalid' | 'type'> export const PasswordInput = forwardRef( - ({ children, className, ...restProps }: PasswordInputProps, ref: ForwardedRef) => ( - - {children} - + ({ className, invalid, ...restProps }: PasswordInputProps, ref: ForwardedRef) => ( + ), ) diff --git a/proprietary/tokens/src/components/ams/password-input.tokens.json b/proprietary/tokens/src/components/ams/password-input.tokens.json index 7123df8c81..a31040b0ed 100644 --- a/proprietary/tokens/src/components/ams/password-input.tokens.json +++ b/proprietary/tokens/src/components/ams/password-input.tokens.json @@ -1,5 +1,30 @@ { "ams": { - "password-input": {} + "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}" } + } + } + } } } diff --git a/storybook/src/components/PasswordInput/PasswordInput.stories.tsx b/storybook/src/components/PasswordInput/PasswordInput.stories.tsx index f7859dab9e..e7b1d4e642 100644 --- a/storybook/src/components/PasswordInput/PasswordInput.stories.tsx +++ b/storybook/src/components/PasswordInput/PasswordInput.stories.tsx @@ -7,10 +7,17 @@ import { PasswordInput } from '@amsterdam/design-system-react/src' import { Meta, StoryObj } from '@storybook/react' const meta = { - title: 'Password Input', + title: 'Components/Forms/Password Input', component: PasswordInput, args: { - children: 'Nieuw component', + disabled: false, + invalid: false, + minLength: 12, + }, + argTypes: { + disabled: { + description: 'Prevents interaction. Avoid if possible.', + }, }, } satisfies Meta From 007f466164272ec0e6d84a9aff3963077bc8e686 Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Fri, 26 Jul 2024 16:34:59 +0200 Subject: [PATCH 03/13] Update password input docs --- packages/css/src/components/password-input/README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/css/src/components/password-input/README.md b/packages/css/src/components/password-input/README.md index 1775c4d128..bd6ad4e015 100644 --- a/packages/css/src/components/password-input/README.md +++ b/packages/css/src/components/password-input/README.md @@ -4,18 +4,21 @@ Helps users enter a password. -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. +## 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. 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. Add a `minlength` attribute to ensure passwords meet a minimum length requirement. + Describe this in the [Field](/docs/components-forms-field--docs)’s description as well. Do not add a `maxlength` attribute. 3. Use the `pattern` attribute to enforce password policies, like including numbers and special characters. - Describe these policies in the [Field](?path=/docs/components-forms-field--docs)’s description as well. + Describe these policies in the [Field](/docs/components-forms-field--docs)’s description as well. 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. 5. Set `autocapitalize="none"`, `autocorrect="off"` and `spellcheck="false"` to stop browsers automatically changing user input. From 58ff4ee66084879591ac457818c84258674896af Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Fri, 26 Jul 2024 16:35:14 +0200 Subject: [PATCH 04/13] Fix placeholder style and token --- packages/css/src/components/password-input/password-input.scss | 2 +- .../tokens/src/components/ams/password-input.tokens.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/css/src/components/password-input/password-input.scss b/packages/css/src/components/password-input/password-input.scss index 108bfe395f..ae2db8daa0 100644 --- a/packages/css/src/components/password-input/password-input.scss +++ b/packages/css/src/components/password-input/password-input.scss @@ -37,7 +37,7 @@ } .ams-password-input::placeholder { - color: var(--ams-text-input-placeholder-color); + color: var(--ams-password-input-placeholder-color); opacity: 100%; // This resets the lower opacity set by Firefox } diff --git a/proprietary/tokens/src/components/ams/password-input.tokens.json b/proprietary/tokens/src/components/ams/password-input.tokens.json index a31040b0ed..1b659b4cfd 100644 --- a/proprietary/tokens/src/components/ams/password-input.tokens.json +++ b/proprietary/tokens/src/components/ams/password-input.tokens.json @@ -24,6 +24,9 @@ "hover": { "box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.primary-red}" } } + }, + "placeholder": { + "color": { "value": "{ams.color.neutral-grey3}" } } } } From 50052b999ec97e7acb6013e2db510edb321016a2 Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Fri, 26 Jul 2024 16:39:33 +0200 Subject: [PATCH 05/13] Add missing tests --- .../src/PasswordInput/PasswordInput.test.tsx | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/react/src/PasswordInput/PasswordInput.test.tsx b/packages/react/src/PasswordInput/PasswordInput.test.tsx index 08fdb73dc1..7e8fb406cb 100644 --- a/packages/react/src/PasswordInput/PasswordInput.test.tsx +++ b/packages/react/src/PasswordInput/PasswordInput.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react' -import { createRef } from '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' @@ -30,6 +31,40 @@ describe('Password input', () => { expect(component).toHaveClass('ams-password-input extra') }) + it('should be working in a controlled state', async () => { + function ControlledComponent() { + const [value, setValue] = useState('Hello') + + return setValue(e.target.value)} /> + } + + const { container } = render() + + 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() + + 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() From 41c1b854f1f61caf1a57d7b573655886810e9dd9 Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Fri, 26 Jul 2024 16:41:07 +0200 Subject: [PATCH 06/13] Support RTL languages for password input --- packages/react/src/PasswordInput/PasswordInput.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react/src/PasswordInput/PasswordInput.tsx b/packages/react/src/PasswordInput/PasswordInput.tsx index 422dcad461..f15b1b9769 100644 --- a/packages/react/src/PasswordInput/PasswordInput.tsx +++ b/packages/react/src/PasswordInput/PasswordInput.tsx @@ -13,11 +13,12 @@ export type PasswordInputProps = { } & Omit, 'aria-invalid' | 'type'> export const PasswordInput = forwardRef( - ({ className, invalid, ...restProps }: PasswordInputProps, ref: ForwardedRef) => ( + ({ className, dir, invalid, ...restProps }: PasswordInputProps, ref: ForwardedRef) => ( From 925a89e6b0736799a964865c189f75cf31a0d001 Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Fri, 26 Jul 2024 16:43:45 +0200 Subject: [PATCH 07/13] Follow our own guidelines for privacy attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And change the value for autocapitalize from ‘none’ to ‘off’ for consistency with autocorrect. --- packages/css/src/components/password-input/README.md | 7 ++++--- packages/react/src/PasswordInput/PasswordInput.tsx | 3 +++ storybook/src/components/TextInput/TextInput.docs.mdx | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/css/src/components/password-input/README.md b/packages/css/src/components/password-input/README.md index bd6ad4e015..2fb27bdc14 100644 --- a/packages/css/src/components/password-input/README.md +++ b/packages/css/src/components/password-input/README.md @@ -10,6 +10,10 @@ Helps users enter a password. 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="off"`, `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. + Consider setting the following attributes: 1. Allow the user’s password manager to automatically fill the password through `autocomplete="current-password"`. @@ -21,8 +25,5 @@ Consider setting the following attributes: Describe these policies in the [Field](/docs/components-forms-field--docs)’s description as well. 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. -5. Set `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. Follow the [guidelines for asking for passwords](https://design-system.service.gov.uk/patterns/passwords/) of the GOV.UK Design System. diff --git a/packages/react/src/PasswordInput/PasswordInput.tsx b/packages/react/src/PasswordInput/PasswordInput.tsx index f15b1b9769..db613793b0 100644 --- a/packages/react/src/PasswordInput/PasswordInput.tsx +++ b/packages/react/src/PasswordInput/PasswordInput.tsx @@ -17,9 +17,12 @@ export const PasswordInput = forwardRef( ), diff --git a/storybook/src/components/TextInput/TextInput.docs.mdx b/storybook/src/components/TextInput/TextInput.docs.mdx index f399a2bf3f..b37ab8d023 100644 --- a/storybook/src/components/TextInput/TextInput.docs.mdx +++ b/storybook/src/components/TextInput/TextInput.docs.mdx @@ -26,7 +26,7 @@ On some devices, it may show an email-specific keyboard. Consider setting the following attributes: 1. Set `autocomplete="email"` to help browsers autofill the user’s email address. -2. Set `autocapitalize="none"`, `autocorrect="off"` and `spellcheck="false"` to stop browsers automatically changing user input. +2. Set `autocapitalize="off"`, `autocorrect="off"` and `spellcheck="false"` to stop browsers automatically changing user input. Email addresses shouldn’t be checked for spelling or grammar. This may also prevent posting the email address to third-party plugins. @@ -46,7 +46,7 @@ On some devices, it may show a URL-specific keyboard to aid in entering web addr Consider setting the following attributes: 1. Set `autocomplete="url"` to help browsers autofill the user’s web address. -2. Set `autocapitalize="none"`, `autocorrect="off"` and `spellcheck="false"` to stop browsers automatically changing user input. +2. Set `autocapitalize="off"`, `autocorrect="off"` and `spellcheck="false"` to stop browsers automatically changing user input. Email addresses shouldn’t be checked for spelling or grammar. This may also prevent posting the web address to third-party plugins. From a476dcd127209aae54d82067cdf38022ab962c42 Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Fri, 26 Jul 2024 16:50:16 +0200 Subject: [PATCH 08/13] Add test for privacy attributes --- .../react/src/PasswordInput/PasswordInput.test.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/react/src/PasswordInput/PasswordInput.test.tsx b/packages/react/src/PasswordInput/PasswordInput.test.tsx index 7e8fb406cb..40b59059a5 100644 --- a/packages/react/src/PasswordInput/PasswordInput.test.tsx +++ b/packages/react/src/PasswordInput/PasswordInput.test.tsx @@ -31,6 +31,16 @@ describe('Password input', () => { expect(component).toHaveClass('ams-password-input extra') }) + it('renders three attributes for privacy', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toHaveAttribute('autocapitalize', 'off') + 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') From f48182d64406eb0d5ba77cbad03b19831035ad6a Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Fri, 26 Jul 2024 16:53:26 +0200 Subject: [PATCH 09/13] Prevent validation in examples as per GOV.UK --- .../src/components/PasswordInput/PasswordInput.stories.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/storybook/src/components/PasswordInput/PasswordInput.stories.tsx b/storybook/src/components/PasswordInput/PasswordInput.stories.tsx index e7b1d4e642..d647d18efd 100644 --- a/storybook/src/components/PasswordInput/PasswordInput.stories.tsx +++ b/storybook/src/components/PasswordInput/PasswordInput.stories.tsx @@ -19,6 +19,13 @@ const meta = { description: 'Prevents interaction. Avoid if possible.', }, }, + decorators: [ + (Story) => ( +
+ + + ), + ], } satisfies Meta export default meta From 4827d915c7dc0ccc4e99c7e3dc63c77d7f88f57d Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Sat, 27 Jul 2024 13:32:25 +0200 Subject: [PATCH 10/13] Prevent overriding privacy props --- packages/css/src/components/password-input/README.md | 1 + packages/react/src/PasswordInput/PasswordInput.tsx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/css/src/components/password-input/README.md b/packages/css/src/components/password-input/README.md index 2fb27bdc14..6de95b6420 100644 --- a/packages/css/src/components/password-input/README.md +++ b/packages/css/src/components/password-input/README.md @@ -13,6 +13,7 @@ Helps users enter a password. This component sets `autocapitalize="off"`, `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: diff --git a/packages/react/src/PasswordInput/PasswordInput.tsx b/packages/react/src/PasswordInput/PasswordInput.tsx index db613793b0..5606cc8640 100644 --- a/packages/react/src/PasswordInput/PasswordInput.tsx +++ b/packages/react/src/PasswordInput/PasswordInput.tsx @@ -10,7 +10,10 @@ import type { ForwardedRef, InputHTMLAttributes } from 'react' export type PasswordInputProps = { /** Whether the value fails a validation rule. */ invalid?: boolean -} & Omit, 'aria-invalid' | 'type'> +} & Omit< + InputHTMLAttributes, + 'aria-invalid' | 'autoCapitalize' | 'autoCorrect' | 'spellCheck' | 'type' +> export const PasswordInput = forwardRef( ({ className, dir, invalid, ...restProps }: PasswordInputProps, ref: ForwardedRef) => ( From 90b2c7ec3d6bb8c841a2083e19716bebf8d8d0f8 Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Sat, 27 Jul 2024 19:24:09 +0200 Subject: [PATCH 11/13] =?UTF-8?q?Use=20`none`=20for=20`autocapitalize`=20?= =?UTF-8?q?=E2=80=93=20`off`=20is=20deprecated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/Attributes.html#//apple_ref/doc/uid/TP40008058-autocapitalize --- packages/css/src/components/password-input/README.md | 2 +- packages/react/src/PasswordInput/PasswordInput.test.tsx | 2 +- packages/react/src/PasswordInput/PasswordInput.tsx | 2 +- storybook/src/components/TextInput/TextInput.docs.mdx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/css/src/components/password-input/README.md b/packages/css/src/components/password-input/README.md index 6de95b6420..3096ccff3d 100644 --- a/packages/css/src/components/password-input/README.md +++ b/packages/css/src/components/password-input/README.md @@ -10,7 +10,7 @@ Helps users enter a password. 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="off"`, `autocorrect="off"` and `spellcheck="false"` to stop browsers automatically changing user input. +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. diff --git a/packages/react/src/PasswordInput/PasswordInput.test.tsx b/packages/react/src/PasswordInput/PasswordInput.test.tsx index 40b59059a5..d66760b0b0 100644 --- a/packages/react/src/PasswordInput/PasswordInput.test.tsx +++ b/packages/react/src/PasswordInput/PasswordInput.test.tsx @@ -36,7 +36,7 @@ describe('Password input', () => { const component = container.querySelector(':only-child') - expect(component).toHaveAttribute('autocapitalize', 'off') + expect(component).toHaveAttribute('autocapitalize', 'none') expect(component).toHaveAttribute('autocorrect', 'off') expect(component).toHaveAttribute('spellcheck', 'false') }) diff --git a/packages/react/src/PasswordInput/PasswordInput.tsx b/packages/react/src/PasswordInput/PasswordInput.tsx index 5606cc8640..3df1d9095c 100644 --- a/packages/react/src/PasswordInput/PasswordInput.tsx +++ b/packages/react/src/PasswordInput/PasswordInput.tsx @@ -20,7 +20,7 @@ export const PasswordInput = forwardRef( Date: Tue, 30 Jul 2024 09:59:06 +0200 Subject: [PATCH 12/13] Advise against the `minlength` attribute as it validates too early --- packages/css/src/components/password-input/README.md | 9 ++++----- .../components/PasswordInput/PasswordInput.stories.tsx | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/css/src/components/password-input/README.md b/packages/css/src/components/password-input/README.md index 3096ccff3d..afe1f37891 100644 --- a/packages/css/src/components/password-input/README.md +++ b/packages/css/src/components/password-input/README.md @@ -19,11 +19,10 @@ 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. Add a `minlength` attribute to ensure passwords meet a minimum length requirement. - Describe this in the [Field](/docs/components-forms-field--docs)’s description as well. - Do not add a `maxlength` attribute. -3. Use the `pattern` attribute to enforce password policies, like including numbers and special characters. - Describe these policies in the [Field](/docs/components-forms-field--docs)’s description as well. +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. diff --git a/storybook/src/components/PasswordInput/PasswordInput.stories.tsx b/storybook/src/components/PasswordInput/PasswordInput.stories.tsx index d647d18efd..063ddbb9eb 100644 --- a/storybook/src/components/PasswordInput/PasswordInput.stories.tsx +++ b/storybook/src/components/PasswordInput/PasswordInput.stories.tsx @@ -12,7 +12,6 @@ const meta = { args: { disabled: false, invalid: false, - minLength: 12, }, argTypes: { disabled: { From 0b1d5f75909aea021d3fff3bc80f47946da0a4c5 Mon Sep 17 00:00:00 2001 From: Vincent Smedinga Date: Tue, 30 Jul 2024 11:43:38 +0200 Subject: [PATCH 13/13] Remove form wrapper again --- .../src/components/PasswordInput/PasswordInput.stories.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/storybook/src/components/PasswordInput/PasswordInput.stories.tsx b/storybook/src/components/PasswordInput/PasswordInput.stories.tsx index 063ddbb9eb..a63801c366 100644 --- a/storybook/src/components/PasswordInput/PasswordInput.stories.tsx +++ b/storybook/src/components/PasswordInput/PasswordInput.stories.tsx @@ -18,13 +18,6 @@ const meta = { description: 'Prevents interaction. Avoid if possible.', }, }, - decorators: [ - (Story) => ( -
- - - ), - ], } satisfies Meta export default meta