-
-
Notifications
You must be signed in to change notification settings - Fork 298
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(form): added a PasswordWithMessage component to be used with use…
…TextField Hook
- Loading branch information
Showing
5 changed files
with
347 additions
and
20 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,55 @@ | ||
import React, { forwardRef, ReactElement } from "react"; | ||
import { | ||
FieldMessageContainerExtension, | ||
FieldMessageContainer, | ||
} from "./FieldMessageContainer"; | ||
|
||
import { Password, PasswordProps } from "./Password"; | ||
|
||
export type PasswordWithMessageProps = FieldMessageContainerExtension< | ||
PasswordProps | ||
>; | ||
|
||
/** | ||
* This component is a simple wrapper for the `Password` and `FormMessage` | ||
* components that should be used along with the `useTextField` hook to | ||
* conditionally show help and error messages with a `Password`. | ||
* | ||
* Simple example: | ||
* | ||
* ```ts | ||
* const [value, fieldProps] = useTextField({ | ||
* id: "field-id", | ||
* required: true, | ||
* minLength: 10, | ||
* }); | ||
* | ||
* return ( | ||
* <PasswordWithMessage | ||
* label="Label" | ||
* placeholder="Placeholder" | ||
* {...fieldProps} | ||
* /> | ||
* ); | ||
* ``` | ||
* | ||
* Note: Unline the `TextFieldWithMessage` and `TextAreaWithMessage`, the error | ||
* icon will do nothing for this component unless the disableVisibility`` prop | ||
* is enabled.` | ||
*/ | ||
export const PasswordWithMessage = forwardRef< | ||
HTMLInputElement, | ||
PasswordWithMessageProps | ||
>(function PasswordWithMessage( | ||
{ messageProps, messageContainerProps, ...props }, | ||
ref | ||
): ReactElement { | ||
return ( | ||
<FieldMessageContainer | ||
{...messageContainerProps} | ||
messageProps={messageProps} | ||
> | ||
<Password {...props} ref={ref} /> | ||
</FieldMessageContainer> | ||
); | ||
}); |
223 changes: 223 additions & 0 deletions
223
packages/form/src/text-field/__tests__/PasswordWithMessage.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,223 @@ | ||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
/* eslint-disable jsx-a11y/aria-role */ | ||
import React, { ReactElement } from "react"; | ||
import { fireEvent, render } from "@testing-library/react"; | ||
|
||
import { PasswordWithMessage } from "../PasswordWithMessage"; | ||
import { TextFieldHookOptions, useTextField } from "../useTextField"; | ||
import { FormMessageProps } from "../FormMessage"; | ||
|
||
function Test({ | ||
id = "field-id", | ||
messageRole, | ||
...options | ||
}: Partial<TextFieldHookOptions> & { messageRole?: "alert" }): ReactElement { | ||
// first arg is value, but it isn't needed for these examples | ||
const [, fieldProps] = useTextField({ | ||
id, | ||
...options, | ||
}); | ||
|
||
let messageProps: FormMessageProps = fieldProps.messageProps; | ||
if (messageRole && messageProps) { | ||
messageProps = { ...messageProps, role: messageRole }; | ||
} | ||
|
||
return ( | ||
<PasswordWithMessage | ||
{...fieldProps} | ||
messageProps={messageProps} | ||
placeholder="Password" | ||
/> | ||
); | ||
} | ||
|
||
describe("PasswordWithMessage", () => { | ||
it("should work with all the defaults when only an id is provided", () => { | ||
const { container, getByPlaceholderText, getByRole } = render(<Test />); | ||
|
||
expect(container).toMatchSnapshot(); | ||
const field = getByPlaceholderText("Password") as HTMLInputElement; | ||
expect(() => getByRole("alert")).toThrow(); | ||
|
||
expect(field).toHaveAttribute("aria-describedby", "field-id-message"); | ||
expect(field.value).toBe(""); | ||
|
||
fireEvent.change(field, { target: { value: "a" } }); | ||
expect(field.value).toBe("a"); | ||
}); | ||
|
||
it("should apply all of the constraint props to the Password", () => { | ||
const { rerender, getByPlaceholderText } = render( | ||
<Test pattern="\d{5,8}" minLength={5} maxLength={8} required /> | ||
); | ||
|
||
const field = getByPlaceholderText("Password"); | ||
expect(field).toHaveAttribute("minLength", "5"); | ||
expect(field).toHaveAttribute("maxLength", "8"); | ||
expect(field).toHaveAttribute("pattern", "\\d{5,8}"); | ||
expect(field).toHaveAttribute("required"); | ||
|
||
rerender(<Test pattern="\d{5,8}" minLength={5} maxLength={8} />); | ||
expect(field).toHaveAttribute("minLength", "5"); | ||
expect(field).toHaveAttribute("maxLength", "8"); | ||
expect(field).toHaveAttribute("pattern", "\\d{5,8}"); | ||
expect(field).not.toHaveAttribute("required"); | ||
}); | ||
|
||
it("should render the counter parts in the message when the counter option is enabled along with the maxLength", () => { | ||
const props = { | ||
counter: true, | ||
maxLength: 20, | ||
}; | ||
|
||
const { rerender, getByPlaceholderText, getByRole } = render( | ||
<Test {...props} messageRole="alert" /> | ||
); | ||
const field = getByPlaceholderText("Password"); | ||
const message = getByRole("alert"); | ||
|
||
expect(field).toHaveAttribute("maxLength", "20"); | ||
expect(message.textContent).toBe("0 / 20"); | ||
|
||
const value = "Hello, world!"; | ||
fireEvent.change(field, { target: { value } }); | ||
expect(message.textContent).toBe(`${value.length} / 20`); | ||
|
||
rerender(<Test {...props} counter={false} />); | ||
expect(field).toHaveAttribute("maxLength", "20"); | ||
expect(message.textContent).toBe(""); | ||
}); | ||
|
||
it("should allow for the maxLength attribute to not be passed to the Password", () => { | ||
const props = { | ||
maxLength: 20, | ||
disableMaxLength: true, | ||
}; | ||
|
||
const { rerender, getByPlaceholderText } = render(<Test {...props} />); | ||
|
||
const field = getByPlaceholderText("Password"); | ||
expect(field).not.toHaveAttribute("maxLength"); | ||
|
||
rerender(<Test {...props} disableMaxLength={false} />); | ||
expect(field).toHaveAttribute("maxLength", "20"); | ||
}); | ||
|
||
it("should enable the error state if the value is less than the minLength or more than the maxLength", () => { | ||
const { getByPlaceholderText, getByRole } = render( | ||
<Test minLength={5} maxLength={20} messageRole="alert" /> | ||
); | ||
const field = getByPlaceholderText("Password"); | ||
const container = field.parentElement!; | ||
const message = getByRole("alert"); | ||
|
||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
|
||
fireEvent.change(field, { target: { value: "1" } }); | ||
expect(container.className).toContain("--error"); | ||
expect(message.className).toContain("--error"); | ||
|
||
fireEvent.change(field, { target: { value: "Valid" } }); | ||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
|
||
fireEvent.change(field, { target: { value: "1234567890123456789" } }); | ||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
|
||
fireEvent.change(field, { target: { value: "12345678901234567890" } }); | ||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
|
||
fireEvent.change(field, { target: { value: "123456789012345678901" } }); | ||
expect(container.className).toContain("--error"); | ||
expect(message.className).toContain("--error"); | ||
}); | ||
|
||
it("should not update the error state on change or update the value if the custon onChange event stopped propagation", () => { | ||
const { getByPlaceholderText, getByRole } = render( | ||
<Test | ||
minLength={10} | ||
onChange={(event) => event.stopPropagation()} | ||
messageRole="alert" | ||
/> | ||
); | ||
|
||
const field = getByPlaceholderText("Password"); | ||
const container = field.parentElement!; | ||
const message = getByRole("alert"); | ||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
|
||
fireEvent.change(field, { target: { value: "1" } }); | ||
expect(field).toHaveAttribute("value", ""); | ||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
}); | ||
|
||
it("should not update the error state on change if `validateOnChange` is false", () => { | ||
const { getByPlaceholderText, getByRole } = render( | ||
<Test minLength={10} validateOnChange={false} messageRole="alert" /> | ||
); | ||
|
||
const field = getByPlaceholderText("Password"); | ||
const container = field.parentElement!; | ||
const message = getByRole("alert"); | ||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
|
||
fireEvent.change(field, { target: { value: "1" } }); | ||
expect(field).toHaveAttribute("value", "1"); | ||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
}); | ||
|
||
it("should not update the error state on change if `validateOnChange` is an empty array", () => { | ||
const { getByPlaceholderText, getByRole } = render( | ||
<Test minLength={10} validateOnChange={[]} messageRole="alert" /> | ||
); | ||
|
||
const field = getByPlaceholderText("Password"); | ||
const container = field.parentElement!; | ||
const message = getByRole("alert"); | ||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
|
||
fireEvent.change(field, { target: { value: "1" } }); | ||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
expect(field).toHaveAttribute("value", "1"); | ||
}); | ||
|
||
it("should render the helpText if when there is no error text", () => { | ||
const { getByPlaceholderText, getByRole } = render( | ||
<Test | ||
helpText="Help Text" | ||
messageRole="alert" | ||
maxLength={5} | ||
getErrorMessage={({ value }) => | ||
value.length > 0 ? "Error Message" : "" | ||
} | ||
/> | ||
); | ||
|
||
const field = getByPlaceholderText("Password"); | ||
const container = field.parentElement!; | ||
const message = getByRole("alert"); | ||
|
||
expect(message.textContent).toBe("Help Text"); | ||
expect(container.className).not.toContain("--error"); | ||
expect(message.className).not.toContain("--error"); | ||
|
||
fireEvent.change(field, { target: { value: "Invalid" } }); | ||
expect(message.textContent).toBe("Error Message"); | ||
expect(container.className).toContain("--error"); | ||
expect(message.className).toContain("--error"); | ||
}); | ||
|
||
it.todo( | ||
"should verify the constraint validation, but it requires a real browser to work" | ||
); | ||
}); |
44 changes: 44 additions & 0 deletions
44
packages/form/src/text-field/__tests__/__snapshots__/PasswordWithMessage.tsx.snap
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,44 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`PasswordWithMessage should work with all the defaults when only an id is provided 1`] = ` | ||
<div> | ||
<div | ||
class="rmd-field-message-container" | ||
> | ||
<div | ||
class="rmd-text-field-container rmd-text-field-container--outline rmd-text-field-container--hoverable rmd-text-field-container--outline-right rmd-password rmd-password--offset" | ||
> | ||
<input | ||
aria-describedby="field-id-message" | ||
class="rmd-text-field rmd-password__input rmd-password__input--offset" | ||
id="field-id" | ||
placeholder="Password" | ||
type="password" | ||
value="" | ||
/> | ||
<button | ||
aria-label="Show password" | ||
aria-pressed="false" | ||
class="rmd-button rmd-button--icon rmd-password__toggle" | ||
id="field-id-password-toggle" | ||
type="button" | ||
> | ||
<i | ||
aria-hidden="true" | ||
class="rmd-icon rmd-icon--font material-icons" | ||
> | ||
remove_red_eye | ||
</i> | ||
<span | ||
class="rmd-ripple-container" | ||
/> | ||
</button> | ||
</div> | ||
<div | ||
aria-live="polite" | ||
class="rmd-form-message rmd-form-message--outline" | ||
id="field-id-message" | ||
/> | ||
</div> | ||
</div> | ||
`; |
Oops, something went wrong.