-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into feat/fields-lg-size
- Loading branch information
Showing
8 changed files
with
977 additions
and
1 deletion.
There are no files selected for viewing
49 changes: 49 additions & 0 deletions
49
documentation/content/components.form.fields.create-password-field.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,49 @@ | ||
--- | ||
slug: create-password-field | ||
title: Create Password Field | ||
links: | ||
viewSource: components/create-password-field | ||
showReportAnIssue: true | ||
tabs: | ||
- content: >- | ||
`CreatePasswordField` is the preferred way to render a form field for | ||
creating a new password. It is built on top of the `PasswordField`. | ||
`CreatePasswordField` accepts the same props as `PasswordInput` but provides default values for some of them (e.g. `label` defaults to `"Create a password"` and `name` defaults to `password`). | ||
`CreatePasswordField` introduces a `validate` prop. `validate` can be synchronous or asynchronous, it allows you to provide a custom function to handle password validation according to your specific requirements. The `defaultValidation` prop allows you to provide an initial validation status that matches the same structure that the validate function returns. This way, you can display immediate feedback to the user while the actual validation is being processed asynchronously. | ||
To provide visual feedback on the validation rules, `CreatePasswordField` utilizes the `InlineMessage` component. The direction of these messages can be controlled using the `messageDirection` prop. | ||
<CodeBlock live={true} preview={true} code={`<Form onSubmit={console.log}> | ||
<CreatePasswordField | ||
validate={(password) => ({ | ||
['7+ characters']: password.length >= 7, | ||
['Is not common password']: !['1234', 'password'].includes(password) | ||
})} | ||
defaultValidation={{ | ||
['7+ characters']: false, | ||
['Is not common password']: false, | ||
}} | ||
messageDirection="column" | ||
css={{ width: 320 }} | ||
/> | ||
</Form>`} language={"tsx"} /> | ||
## API Reference | ||
<ComponentProps component="CreatePasswordField" /> | ||
title: Main | ||
parent: ru0Ovr_U82kdQX8m3WahL | ||
uuid: MD1mmrf40ecDo4z16-bco | ||
nestedSlug: | ||
- components | ||
- form | ||
- fields | ||
- create-password-field | ||
--- |
91 changes: 91 additions & 0 deletions
91
lib/src/components/create-password-field/CreatePasswordField.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,91 @@ | ||
import { render, screen } from '@testing-library/react' | ||
import userEvent from '@testing-library/user-event' | ||
import { axe } from 'jest-axe' | ||
import * as React from 'react' | ||
|
||
import { Button } from '../button' | ||
import { Form } from '../form' | ||
import { Tooltip } from '../tooltip' | ||
import { CreatePasswordField } from '.' | ||
|
||
const props: React.ComponentProps<typeof CreatePasswordField> = { | ||
name: 'password', | ||
label: 'Create a password', | ||
defaultValidation: { | ||
'7+ characters': false, | ||
'Contains number': false, | ||
'Contains symbol': false | ||
}, | ||
validate: () => | ||
Promise.resolve({ | ||
'7+ characters': true, | ||
'Contains number': false, | ||
'Contains symbol': true | ||
}) | ||
} | ||
|
||
const customRender = () => | ||
render( | ||
<Tooltip.Provider> | ||
<Form onSubmit={jest.fn()}> | ||
<CreatePasswordField {...props} /> | ||
<Button type="submit">Submit</Button> | ||
</Form> | ||
</Tooltip.Provider> | ||
) | ||
|
||
describe(`CreatePasswordField component`, () => { | ||
it('renders', () => { | ||
const { container } = customRender() | ||
|
||
expect(container).toMatchSnapshot() | ||
}) | ||
|
||
it('has no programmatically detectable a11y issues', async () => { | ||
const { container } = customRender() | ||
|
||
expect(await axe(container)).toHaveNoViolations() | ||
}) | ||
|
||
it('displays validation messages when focusing on the input', () => { | ||
customRender() | ||
|
||
expect(screen.queryByText('7+ characters')).not.toBeInTheDocument() | ||
expect(screen.queryByText('Contains number')).not.toBeInTheDocument() | ||
expect(screen.queryByText('Contains symbol')).not.toBeInTheDocument() | ||
|
||
userEvent.click(screen.getByLabelText('Create a password')) | ||
|
||
expect(screen.getByText('7+ characters')).toBeVisible() | ||
expect(screen.getByText('Contains number')).toBeVisible() | ||
expect(screen.getByText('Contains symbol')).toBeVisible() | ||
}) | ||
|
||
it('displays validation messages on blur after the form has been touched', () => { | ||
customRender() | ||
|
||
const passwordInput = screen.getByText('Create a password') | ||
userEvent.type(passwordInput, 'password') | ||
userEvent.tab() | ||
|
||
expect(screen.getByText('7+ characters')).toBeVisible() | ||
expect(screen.getByText('Contains number')).toBeVisible() | ||
expect(screen.getByText('Contains symbol')).toBeVisible() | ||
}) | ||
|
||
it('displays validation messages when trying to submit the form', async () => { | ||
customRender() | ||
|
||
userEvent.click(screen.getByRole('button', { name: 'Submit' })) | ||
|
||
expect(await screen.findByText('7+ characters')).toBeVisible() | ||
}) | ||
|
||
it('displays validation styling to the input when the value is not valid', () => { | ||
const { container } = customRender() | ||
|
||
userEvent.click(screen.getByRole('button', { name: 'Submit' })) | ||
|
||
expect(container).toMatchSnapshot() | ||
}) | ||
}) |
112 changes: 112 additions & 0 deletions
112
lib/src/components/create-password-field/CreatePasswordField.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,112 @@ | ||
import invariant from 'invariant' | ||
import * as React from 'react' | ||
import { useFormContext } from 'react-hook-form' | ||
import { throttle } from 'throttle-debounce' | ||
|
||
import type { CSS } from '~/stitches' | ||
import { Override } from '~/utilities' | ||
|
||
import { Box } from '../box' | ||
import { FieldElementWrapperProps } from '../field-wrapper' | ||
import { Flex } from '../flex' | ||
import { InlineMessage } from '../inline-message' | ||
import { PasswordField } from '../password-field' | ||
import { PasswordInput } from '../password-input' | ||
|
||
type ValidationResult = Record<string, boolean> | ||
|
||
type CreatePasswordFieldProps = Override< | ||
React.ComponentProps<typeof PasswordInput> & FieldElementWrapperProps, | ||
{ | ||
label?: string | ||
name?: string | ||
validate: ( | ||
password: string | ||
) => Promise<ValidationResult | undefined> | ValidationResult | ||
defaultValidation: ValidationResult | ||
messageDirection?: CSS['flexDirection'] | ||
} | ||
> | ||
|
||
export const CreatePasswordField = ({ | ||
validate, | ||
defaultValidation, | ||
messageDirection = 'row', | ||
label = 'Create a password', | ||
name = 'password', | ||
css, | ||
validation, | ||
...remainingProps | ||
}: CreatePasswordFieldProps) => { | ||
const { formState } = useFormContext() | ||
const [isFocused, setIsFocused] = React.useState<boolean>(false) | ||
const [validationResult, setValidationResult] = | ||
React.useState<ValidationResult>(defaultValidation) | ||
|
||
const touched = formState.touched[name] | ||
const error = formState.errors[name]?.type === 'validate' | ||
|
||
const validatePassword = React.useCallback( | ||
async (password: string) => { | ||
const result = await validate(password) | ||
|
||
if (result) { | ||
invariant( | ||
typeof result === 'object', | ||
'The validate function must return an object' | ||
) | ||
|
||
setValidationResult(result) | ||
return Object.values(result).every((isValid) => isValid) | ||
} | ||
|
||
return false | ||
}, | ||
[setValidationResult] | ||
) | ||
|
||
const handleChange = React.useCallback(throttle(500, validatePassword), [ | ||
validatePassword | ||
]) | ||
|
||
const getMessageTheme = (result: boolean, isFocused: boolean) => { | ||
if (result) return 'success' | ||
|
||
return isFocused ? 'neutral' : 'error' | ||
} | ||
|
||
return ( | ||
<Box css={css}> | ||
<PasswordField | ||
label={label} | ||
name={name} | ||
onChange={(e) => handleChange(e.target.value)} | ||
onBlur={() => setIsFocused(false)} | ||
onFocus={() => setIsFocused(true)} | ||
validation={{ ...validation, validate: validatePassword }} | ||
{...remainingProps} | ||
/> | ||
{(touched || isFocused || error) && ( | ||
<Flex | ||
css={{ | ||
mt: '$2', | ||
gap: '$2', | ||
flexWrap: 'wrap', | ||
flexDirection: messageDirection | ||
}} | ||
> | ||
{Object.entries(validationResult).map(([message, result]) => ( | ||
<InlineMessage | ||
key={message} | ||
theme={getMessageTheme(result, isFocused)} | ||
> | ||
{message} | ||
</InlineMessage> | ||
))} | ||
</Flex> | ||
)} | ||
</Box> | ||
) | ||
} | ||
|
||
CreatePasswordField.displayName = 'CreatePasswordField' |
Oops, something went wrong.