@purpurds/password-field 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/LICENSE.txt +92 -0
  2. package/dist/constants.d.ts +10 -0
  3. package/dist/constants.d.ts.map +1 -0
  4. package/dist/main-B6vgbdqW.mjs +1002 -0
  5. package/dist/main-B6vgbdqW.mjs.map +1 -0
  6. package/dist/main-CqBhSgIE.js +2 -0
  7. package/dist/main-CqBhSgIE.js.map +1 -0
  8. package/dist/metadata.js +9 -0
  9. package/dist/password-field-BwuKVABM.mjs +1557 -0
  10. package/dist/password-field-BwuKVABM.mjs.map +1 -0
  11. package/dist/password-field-CE2wn3y4.js +58 -0
  12. package/dist/password-field-CE2wn3y4.js.map +1 -0
  13. package/dist/password-field.cjs.js +2 -0
  14. package/dist/password-field.cjs.js.map +1 -0
  15. package/dist/password-field.d.ts +27 -0
  16. package/dist/password-field.d.ts.map +1 -0
  17. package/dist/password-field.es.js +8 -0
  18. package/dist/password-field.es.js.map +1 -0
  19. package/dist/password-strength-indicator.d.ts +11 -0
  20. package/dist/password-strength-indicator.d.ts.map +1 -0
  21. package/dist/password-strength.d.ts +13 -0
  22. package/dist/password-strength.d.ts.map +1 -0
  23. package/dist/styles.css +1 -0
  24. package/dist/types.d.ts +7 -0
  25. package/dist/types.d.ts.map +1 -0
  26. package/dist/use-password-strength.d.ts +10 -0
  27. package/dist/use-password-strength.d.ts.map +1 -0
  28. package/package.json +69 -0
  29. package/src/constants.ts +10 -0
  30. package/src/global.d.ts +4 -0
  31. package/src/password-field.module.scss +5 -0
  32. package/src/password-field.stories.tsx +273 -0
  33. package/src/password-field.story.css +23 -0
  34. package/src/password-field.test.tsx +69 -0
  35. package/src/password-field.tsx +128 -0
  36. package/src/password-strength-indicator.module.scss +19 -0
  37. package/src/password-strength-indicator.test.tsx +46 -0
  38. package/src/password-strength-indicator.tsx +38 -0
  39. package/src/password-strength.module.scss +30 -0
  40. package/src/password-strength.test.tsx +52 -0
  41. package/src/password-strength.tsx +79 -0
  42. package/src/types.ts +7 -0
  43. package/src/use-password-strength.ts +61 -0
@@ -0,0 +1,273 @@
1
+ import React, { ChangeEvent } from "react";
2
+ import { Button } from "@purpurds/button";
3
+ import { Heading } from "@purpurds/heading";
4
+ import { Paragraph } from "@purpurds/paragraph";
5
+ import { useArgs } from "@storybook/preview-api";
6
+ import type { Meta, StoryObj } from "@storybook/react";
7
+
8
+ import "@purpurds/button/styles";
9
+ import "@purpurds/field-error-text/styles";
10
+ import "@purpurds/heading/styles";
11
+ import "@purpurds/icon/styles";
12
+ import "@purpurds/paragraph/styles";
13
+ import "@purpurds/text-field/styles";
14
+ import "./password-field.story.css";
15
+ import { PasswordField } from "./password-field";
16
+
17
+ const PasswordFieldStoryBookComponent = (...args: any[]) => {
18
+ const [password, setPassword] = React.useState("");
19
+ const [passwordTwo, setPasswordTwo] = React.useState("");
20
+
21
+ const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
22
+ setPassword(e.target.value);
23
+ };
24
+ const handlePasswordTwoChange = (e: ChangeEvent<HTMLInputElement>) => {
25
+ setPasswordTwo(e.target.value);
26
+ };
27
+
28
+ const isValid = !!password && password === passwordTwo;
29
+ return (
30
+ <form className="password-field-story-form" onSubmit={(e) => e.preventDefault()}>
31
+ <div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
32
+ <Heading tag="h1" variant="title-400">
33
+ New password
34
+ </Heading>
35
+ <Paragraph>
36
+ Choose a new password with at least 10 characters. Make sure to select a strong password
37
+ for increased security.
38
+ </Paragraph>
39
+ </div>
40
+ <div className="password-field-story-field-container">
41
+ <PasswordField
42
+ {...args}
43
+ label="Password"
44
+ name="password"
45
+ showPasswordAllyLabel="Show password"
46
+ hidePasswordAllyLabel="Hide password"
47
+ errorText=""
48
+ value={password}
49
+ valid={isValid}
50
+ onChange={handlePasswordChange}
51
+ >
52
+ <PasswordField.PasswordStrength
53
+ value={password}
54
+ label="Password strength"
55
+ weakText="Weak"
56
+ mediumText="Medium"
57
+ strongText="Strong"
58
+ />
59
+ </PasswordField>
60
+ <PasswordField
61
+ {...args}
62
+ label="Confirm password"
63
+ name="confirm-password"
64
+ showPasswordAllyLabel="Show password"
65
+ hidePasswordAllyLabel="Hide password"
66
+ errorText=""
67
+ value={passwordTwo}
68
+ valid={isValid}
69
+ onChange={handlePasswordTwoChange}
70
+ />
71
+ </div>
72
+ <div className="password-field-story-button-container">
73
+ <Button variant="primary" type="submit">
74
+ Save password
75
+ </Button>
76
+ <Button variant="secondary" type="button">
77
+ Go back
78
+ </Button>
79
+ </div>
80
+ </form>
81
+ );
82
+ };
83
+
84
+ const meta = {
85
+ title: "Inputs/PasswordField",
86
+ component: PasswordField,
87
+ subcomponents: {
88
+ //@ts-ignore
89
+ "PasswordField.PasswordStrength": PasswordField.PasswordStrength,
90
+ },
91
+ parameters: {
92
+ design: [
93
+ {
94
+ name: "PasswordField",
95
+ type: "figma",
96
+ url: "https://www.figma.com/design/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?node-id=67888-101414&m=dev",
97
+ },
98
+ ],
99
+ },
100
+ args: {
101
+ disabled: false,
102
+ errorText: undefined,
103
+ helperText: "Enter your password",
104
+ hidePasswordAllyLabel: "Hide password",
105
+ label: "Password",
106
+ loading: false,
107
+ placeholder: "Enter text",
108
+ readOnly: false,
109
+ required: true,
110
+ showPasswordAllyLabel: "Show password",
111
+ valid: false,
112
+ value: "",
113
+ },
114
+ } satisfies Meta<typeof PasswordField>;
115
+
116
+ export default meta;
117
+
118
+ type Story = StoryObj<typeof PasswordField>;
119
+
120
+ export const Showcase: Story = {
121
+ parameters: {
122
+ backgrounds: {
123
+ values: [
124
+ { name: "light", value: "#ffffff" },
125
+ { name: "dark", value: "#000000" },
126
+ { name: "lightgray", value: "#dfdfdf" },
127
+ ],
128
+ default: "lightgray",
129
+ },
130
+ docs: {
131
+ source: {
132
+ code: `
133
+ <form>
134
+ <div>
135
+ <Heading tag="h1" variant="title-400">
136
+ New password
137
+ </Heading>
138
+ <Paragraph>
139
+ Choose a new password with at least 10 characters. Make sure to select a strong password
140
+ for increased security.
141
+ </Paragraph>
142
+ </div>
143
+ <div>
144
+ <PasswordField
145
+ {...args}
146
+ label="Password"
147
+ name="password"
148
+ showPasswordAllyLabel="Show password"
149
+ hidePasswordAllyLabel="Hide password"
150
+ errorText=""
151
+ value={password}
152
+ valid={isValid}
153
+ onChange={handlePasswordChange}
154
+ >
155
+ <PasswordField.PasswordStrength
156
+ value={password}
157
+ label="Password strength"
158
+ weakText="Weak"
159
+ mediumText="Medium"
160
+ strongText="Strong"
161
+ />
162
+ </PasswordField>
163
+ <PasswordField
164
+ {...args}
165
+ label="Confirm password"
166
+ name="confirm-password"
167
+ showPasswordAllyLabel="Show password"
168
+ hidePasswordAllyLabel="Hide password"
169
+ errorText=""
170
+ value={passwordTwo}
171
+ valid={isValid}
172
+ onChange={handlePasswordTwoChange}
173
+ />
174
+ </div>
175
+ <div>
176
+ <Button variant="primary" type="submit">
177
+ Save password
178
+ </Button>
179
+ <Button variant="secondary" type="button">
180
+ Go back
181
+ </Button>
182
+ </div>
183
+ </form>
184
+ `,
185
+ },
186
+ },
187
+ },
188
+ render: ({ ...args }) => {
189
+ return <PasswordFieldStoryBookComponent {...args} />;
190
+ },
191
+ };
192
+
193
+ export const OnlyPasswordField: Story = {
194
+ parameters: {
195
+ docs: {
196
+ source: {
197
+ code: `
198
+ <PasswordField
199
+ value={value}
200
+ onChange={setValue}
201
+ disabled={false}
202
+ errorText={undefined}
203
+ helperText="Enter your password"
204
+ hidePasswordAllyLabel="Hide password"
205
+ label="Password"
206
+ placeholder="Enter text"
207
+ required={true}
208
+ showPasswordAllyLabel="Show password"
209
+ />
210
+ `,
211
+ },
212
+ },
213
+ },
214
+ render: ({ ...args }) => {
215
+ const [{ value }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
216
+
217
+ const setValue = (e: ChangeEvent<HTMLInputElement>) => {
218
+ updateArgs({ value: e.target.value });
219
+ };
220
+
221
+ return <PasswordField {...args} value={value} onChange={setValue} />;
222
+ },
223
+ };
224
+
225
+ export const PasswordFieldWithPasswordStrength: Story = {
226
+ parameters: {
227
+ docs: {
228
+ source: {
229
+ code: `
230
+ <PasswordField
231
+ value={value}
232
+ onChange={setValue}
233
+ disabled={false}
234
+ errorText={undefined}
235
+ helperText="Enter your password"
236
+ hidePasswordAllyLabel="Hide password"
237
+ label="Password"
238
+ placeholder="Enter text"
239
+ required={true}
240
+ showPasswordAllyLabel="Show password"
241
+ >
242
+ <PasswordField.PasswordStrength
243
+ value={value}
244
+ label="Password strength"
245
+ weakText="Weak"
246
+ mediumText="Medium"
247
+ strongText="Strong"
248
+ />
249
+ </PasswordField>
250
+ `,
251
+ },
252
+ },
253
+ },
254
+ render: ({ ...args }) => {
255
+ const [{ value }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
256
+
257
+ const setValue = (e: ChangeEvent<HTMLInputElement>) => {
258
+ updateArgs({ value: e.target.value });
259
+ };
260
+
261
+ return (
262
+ <PasswordField {...args} value={value} onChange={setValue}>
263
+ <PasswordField.PasswordStrength
264
+ value={value}
265
+ label="Password strength"
266
+ weakText="Weak"
267
+ mediumText="Medium"
268
+ strongText="Strong"
269
+ />
270
+ </PasswordField>
271
+ );
272
+ },
273
+ };
@@ -0,0 +1,23 @@
1
+ .password-field-story-form {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--purpur-spacing-400);
5
+ max-width: calc(4 * var(--purpur-spacing-1200));
6
+ width: 100%;
7
+ margin: var(--purpur-spacing-400) auto;
8
+ padding: var(--purpur-spacing-400);
9
+ border-radius: var(--purpur-border-radius-lg);
10
+ background-color: var(--purpur-color-background-secondary);
11
+ }
12
+
13
+ .password-field-story-field-container {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: var(--purpur-spacing-300);
17
+ }
18
+
19
+ .password-field-story-button-container {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: var(--purpur-spacing-200);
23
+ }
@@ -0,0 +1,69 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, render, screen } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import { PasswordField, PasswordFieldProps } from "./password-field";
8
+
9
+ expect.extend(matchers);
10
+
11
+ afterEach(() => {
12
+ cleanup();
13
+ });
14
+
15
+ describe("PasswordField", () => {
16
+ const defaultProps: PasswordFieldProps = {
17
+ clearButtonAllyLabel: "Clear",
18
+ showPasswordAllyLabel: "Show password",
19
+ hidePasswordAllyLabel: "Hide password",
20
+ onClear: vi.fn(),
21
+ };
22
+
23
+ it("renders without crashing", () => {
24
+ render(<PasswordField {...defaultProps} />);
25
+ const component = screen.getByTestId(Selectors.PasswordField);
26
+ expect(component).toBeInTheDocument();
27
+ });
28
+
29
+ it("toggles password visibility when the visibility button is clicked", async () => {
30
+ render(<PasswordField {...defaultProps} />);
31
+ const typeToggleButton = screen.getByTestId(Selectors.TypeToggleButton);
32
+ const inputField = screen.getByTestId(Selectors.PasswordField);
33
+
34
+ // Initially, the input type should be password
35
+ expect(inputField).toHaveAttribute("type", "password");
36
+
37
+ // Click the visibility button to show the password
38
+ await userEvent.click(typeToggleButton);
39
+ expect(inputField).toHaveAttribute("type", "text");
40
+
41
+ // Click the visibility button again to hide the password
42
+ await userEvent.click(typeToggleButton);
43
+ expect(inputField).toHaveAttribute("type", "password");
44
+ });
45
+
46
+ it("calls onChange when the input value changes", async () => {
47
+ const handleChange = vi.fn();
48
+ render(<PasswordField {...defaultProps} onChange={handleChange} />);
49
+ const inputField = screen.getByTestId(Selectors.PasswordField);
50
+
51
+ await userEvent.type(inputField, "new password");
52
+ expect(handleChange).toHaveBeenCalled();
53
+ });
54
+
55
+ it("renders children correctly", () => {
56
+ render(
57
+ <PasswordField {...defaultProps}>
58
+ <div data-testid="child">Child Component</div>
59
+ </PasswordField>
60
+ );
61
+ const childComponent = screen.getByTestId("child");
62
+ expect(childComponent).toBeInTheDocument();
63
+ });
64
+ });
65
+
66
+ const Selectors = {
67
+ PasswordField: "password-field-input",
68
+ TypeToggleButton: "password-field-type-toggle-button",
69
+ };
@@ -0,0 +1,128 @@
1
+ import React, {
2
+ ChangeEvent,
3
+ ForwardedRef,
4
+ forwardRef,
5
+ isValidElement,
6
+ ReactElement,
7
+ ReactNode,
8
+ ReactPortal,
9
+ useState,
10
+ } from "react";
11
+ import { Button } from "@purpurds/button";
12
+ import { IconPasswordInvisible } from "@purpurds/icon/password-invisible";
13
+ import { IconPasswordVisible } from "@purpurds/icon/password-visible";
14
+ import { type TextFieldProps, TextField } from "@purpurds/text-field";
15
+ import c from "classnames/bind";
16
+
17
+ import { FIELD_TYPE } from "./constants";
18
+ import styles from "./password-field.module.scss";
19
+ import { PasswordStrength } from "./password-strength";
20
+ import { FieldType } from "./types";
21
+
22
+ const cx = c.bind(styles);
23
+
24
+ export type PasswordFieldDefaultProps = {
25
+ ["data-testid"]?: string;
26
+ children?: ReactNode;
27
+ className?: string;
28
+ showPasswordAllyLabel: string;
29
+ hidePasswordAllyLabel: string;
30
+ onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
31
+ };
32
+
33
+ type TextFieldClearProps =
34
+ | {
35
+ clearButtonAllyLabel: string;
36
+ onClear: () => void;
37
+ }
38
+ | {
39
+ clearButtonAllyLabel?: never;
40
+ onClear?: never;
41
+ };
42
+
43
+ export type PasswordFieldProps = PasswordFieldDefaultProps &
44
+ Omit<TextFieldProps, "startAdornment" | "endAdornment" | "afterField" | "type"> &
45
+ TextFieldClearProps;
46
+
47
+ const rootClassName = "purpur-password-field";
48
+
49
+ type PasswordFieldCmp<P> = React.ForwardRefExoticComponent<
50
+ P & React.RefAttributes<HTMLInputElement>
51
+ > & {
52
+ PasswordStrength: typeof PasswordStrength;
53
+ };
54
+
55
+ export const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
56
+ (
57
+ {
58
+ ["data-testid"]: dataTestId = "password-field",
59
+ children,
60
+ className,
61
+ hidePasswordAllyLabel,
62
+ onChange,
63
+ showPasswordAllyLabel,
64
+ ...props
65
+ }: PasswordFieldProps,
66
+ ref: ForwardedRef<HTMLInputElement>
67
+ ) => {
68
+ const classes = cx([className, rootClassName]);
69
+ const [type, setType] = useState<FieldType>(FIELD_TYPE.PASSWORD);
70
+
71
+ const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
72
+ onChange?.(event);
73
+ };
74
+
75
+ const handleVisibilityChange = () => {
76
+ setType(type === FIELD_TYPE.PASSWORD ? FIELD_TYPE.TEXT : FIELD_TYPE.PASSWORD);
77
+ };
78
+
79
+ const IconComponent =
80
+ type === FIELD_TYPE.PASSWORD ? IconPasswordVisible : IconPasswordInvisible;
81
+
82
+ return (
83
+ <div className={classes}>
84
+ <TextField
85
+ className={cx(`${rootClassName}__text-field`)}
86
+ data-testid={dataTestId}
87
+ onChange={handleChange}
88
+ ref={ref}
89
+ type={type}
90
+ endAdornment={
91
+ // eslint-disable-next-line react/jsx-wrap-multilines
92
+ <Button
93
+ aria-label={
94
+ type === FIELD_TYPE.PASSWORD ? showPasswordAllyLabel : hidePasswordAllyLabel
95
+ }
96
+ data-testid={`${dataTestId}-type-toggle-button`}
97
+ onClick={handleVisibilityChange}
98
+ size="sm"
99
+ variant="tertiary-purple"
100
+ iconOnly
101
+ >
102
+ <IconComponent size="sm" />
103
+ </Button>
104
+ }
105
+ {...props}
106
+ />
107
+ {children}
108
+ </div>
109
+ );
110
+ }
111
+ ) as PasswordFieldCmp<PasswordFieldProps>;
112
+
113
+ PasswordField.PasswordStrength = PasswordStrength;
114
+
115
+ PasswordField.displayName = "PasswordField";
116
+
117
+ export const isPasswordStrength = (
118
+ child:
119
+ | ReactElement
120
+ | Iterable<ReactNode>
121
+ | ReactPortal
122
+ | string
123
+ | number
124
+ | boolean
125
+ | null
126
+ | undefined
127
+ ): child is ReactElement<PasswordFieldProps> =>
128
+ isValidElement<PasswordFieldProps>(child) && child?.type === PasswordStrength;
@@ -0,0 +1,19 @@
1
+ .purpur-password-strength-indicator {
2
+ $root: &;
3
+ background-color: var(--purpur-color-background-interactive-disabled);
4
+ border-radius: var(--purpur-border-radius-full);
5
+ height: var(--purpur-spacing-100);
6
+ width: 100%;
7
+
8
+ &--active {
9
+ &#{$root}--weak {
10
+ background-color: var(--purpur-color-background-status-error-strong);
11
+ }
12
+ &#{$root}--medium {
13
+ background-color: var(--purpur-color-background-status-warning-strong);
14
+ }
15
+ &#{$root}--strong {
16
+ background-color: var(--purpur-color-background-status-success-strong);
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, render, screen } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+
6
+ import { PASSWORD_STRENGTH } from "./constants";
7
+ import { PasswordStrengthIndicator } from "./password-strength-indicator";
8
+ import type { PasswordStrengthValue } from "./types";
9
+
10
+ expect.extend(matchers);
11
+
12
+ afterEach(() => {
13
+ cleanup();
14
+ });
15
+
16
+ describe("PasswordStrengthIndicator", () => {
17
+ it("renders without crashing", () => {
18
+ render(<PasswordStrengthIndicator active={false} />);
19
+ const indicator = screen.getByTestId(Selectors.PasswordStrengthIndicator);
20
+ expect(indicator).toBeInTheDocument();
21
+ });
22
+
23
+ it("applies the active class when active is true", () => {
24
+ render(<PasswordStrengthIndicator active={true} />);
25
+ const indicator = screen.getByTestId(Selectors.PasswordStrengthIndicator);
26
+ expect(indicator).toHaveClass("purpur-password-strength-indicator--active");
27
+ });
28
+
29
+ it("applies the correct strength class when passwordStrength is provided", () => {
30
+ const strength: PasswordStrengthValue = PASSWORD_STRENGTH.STRONG;
31
+ render(<PasswordStrengthIndicator active={false} passwordStrength={strength} />);
32
+ const indicator = screen.getByTestId(Selectors.PasswordStrengthIndicator);
33
+ expect(indicator).toHaveClass(`purpur-password-strength-indicator--${strength}`);
34
+ });
35
+
36
+ it("applies additional className if provided", () => {
37
+ const customClass = "custom-class";
38
+ render(<PasswordStrengthIndicator active={false} className={customClass} />);
39
+ const indicator = screen.getByTestId(Selectors.PasswordStrengthIndicator);
40
+ expect(indicator).toHaveClass(customClass);
41
+ });
42
+ });
43
+
44
+ const Selectors = {
45
+ PasswordStrengthIndicator: "password-strength-indicator",
46
+ };
@@ -0,0 +1,38 @@
1
+ import React, { ForwardedRef, forwardRef } from "react";
2
+ import c from "classnames/bind";
3
+
4
+ import styles from "./password-strength-indicator.module.scss";
5
+ import type { PasswordStrengthValue } from "./types";
6
+ const cx = c.bind(styles);
7
+
8
+ export type PasswordStrengthIndicatorProps = {
9
+ ["data-testid"]?: string;
10
+ active: boolean;
11
+ className?: string;
12
+ passwordStrength?: PasswordStrengthValue;
13
+ };
14
+
15
+ const rootClassName = "purpur-password-strength-indicator";
16
+
17
+ export const PasswordStrengthIndicator = forwardRef(
18
+ (
19
+ {
20
+ ["data-testid"]: dataTestId = "password-strength-indicator",
21
+ active,
22
+ className,
23
+ passwordStrength,
24
+ }: PasswordStrengthIndicatorProps,
25
+ ref: ForwardedRef<HTMLDivElement>
26
+ ) => {
27
+ const classes = cx([
28
+ className,
29
+ rootClassName,
30
+ { [`${rootClassName}--${passwordStrength}`]: passwordStrength },
31
+ { [`${rootClassName}--active`]: active },
32
+ ]);
33
+
34
+ return <div data-testid={dataTestId} className={classes} ref={ref} />;
35
+ }
36
+ );
37
+
38
+ PasswordStrengthIndicator.displayName = "PasswordStrengthIndicator";
@@ -0,0 +1,30 @@
1
+ .purpur-password-strength {
2
+ $root: &;
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--purpur-spacing-100);
6
+
7
+ &__indicator-container {
8
+ display: flex;
9
+ gap: var(--purpur-spacing-100);
10
+ }
11
+
12
+ #{$root}__password-strength-text {
13
+ &--weak {
14
+ color: var(--purpur-color-text-status-error-strong);
15
+ }
16
+ &--medium {
17
+ color: var(--purpur-color-text-status-warning-strong);
18
+ }
19
+ &--strong {
20
+ color: var(--purpur-color-text-status-success-strong);
21
+ }
22
+ }
23
+
24
+ #{$root}__password-strength-label {
25
+ font-family: var(--purpur-typography-family-default);
26
+ font-size: var(--purpur-typography-scale-100);
27
+ font-weight: var(--purpur-typography-weight-medium);
28
+ line-height: var(--purpur-typography-line-height-loose);
29
+ }
30
+ }
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+
6
+ import { PasswordStrength } from "./password-strength";
7
+
8
+ expect.extend(matchers);
9
+
10
+ afterEach(() => {
11
+ cleanup();
12
+ });
13
+
14
+ describe("PasswordStrength", () => {
15
+ const defaultProps = {
16
+ value: "p4ssWord123!!!",
17
+ label: "Password strength",
18
+ weakText: "Weak",
19
+ mediumText: "Medium",
20
+ strongText: "Strong",
21
+ };
22
+
23
+ it("renders without crashing", () => {
24
+ render(<PasswordStrength {...defaultProps} />);
25
+ const component = screen.getByTestId(Selectors.PasswordStrength);
26
+ expect(component).toBeInTheDocument();
27
+ });
28
+
29
+ it("displays the correct password strength text", async () => {
30
+ render(<PasswordStrength {...defaultProps} />);
31
+ const strengthText = screen.getByTestId(Selectors.PasswordStrengthText);
32
+ await waitFor(() => expect(strengthText).toHaveTextContent("Strong"));
33
+ });
34
+
35
+ it("renders the correct number of PasswordStrengthIndicator components", () => {
36
+ render(<PasswordStrength {...defaultProps} />);
37
+ const indicators = screen.getAllByTestId(Selectors.PasswordStrengthIndicator);
38
+ expect(indicators.length).toBe(3);
39
+ });
40
+
41
+ it("applies the correct class names based on password strength", () => {
42
+ render(<PasswordStrength {...defaultProps} />);
43
+ const component = screen.getByTestId(Selectors.PasswordStrength);
44
+ expect(component).toHaveClass("purpur-password-strength");
45
+ });
46
+ });
47
+
48
+ const Selectors = {
49
+ PasswordStrength: "password-strength",
50
+ PasswordStrengthText: "password-strength-text",
51
+ PasswordStrengthIndicator: "password-strength-indicator",
52
+ };