@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.
- package/dist/LICENSE.txt +92 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/main-B6vgbdqW.mjs +1002 -0
- package/dist/main-B6vgbdqW.mjs.map +1 -0
- package/dist/main-CqBhSgIE.js +2 -0
- package/dist/main-CqBhSgIE.js.map +1 -0
- package/dist/metadata.js +9 -0
- package/dist/password-field-BwuKVABM.mjs +1557 -0
- package/dist/password-field-BwuKVABM.mjs.map +1 -0
- package/dist/password-field-CE2wn3y4.js +58 -0
- package/dist/password-field-CE2wn3y4.js.map +1 -0
- package/dist/password-field.cjs.js +2 -0
- package/dist/password-field.cjs.js.map +1 -0
- package/dist/password-field.d.ts +27 -0
- package/dist/password-field.d.ts.map +1 -0
- package/dist/password-field.es.js +8 -0
- package/dist/password-field.es.js.map +1 -0
- package/dist/password-strength-indicator.d.ts +11 -0
- package/dist/password-strength-indicator.d.ts.map +1 -0
- package/dist/password-strength.d.ts +13 -0
- package/dist/password-strength.d.ts.map +1 -0
- package/dist/styles.css +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/use-password-strength.d.ts +10 -0
- package/dist/use-password-strength.d.ts.map +1 -0
- package/package.json +69 -0
- package/src/constants.ts +10 -0
- package/src/global.d.ts +4 -0
- package/src/password-field.module.scss +5 -0
- package/src/password-field.stories.tsx +273 -0
- package/src/password-field.story.css +23 -0
- package/src/password-field.test.tsx +69 -0
- package/src/password-field.tsx +128 -0
- package/src/password-strength-indicator.module.scss +19 -0
- package/src/password-strength-indicator.test.tsx +46 -0
- package/src/password-strength-indicator.tsx +38 -0
- package/src/password-strength.module.scss +30 -0
- package/src/password-strength.test.tsx +52 -0
- package/src/password-strength.tsx +79 -0
- package/src/types.ts +7 -0
- 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
|
+
};
|