@virastack/password 1.0.2 → 1.1.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/README.md +135 -16
- package/dist/index.cjs +351 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +201 -0
- package/dist/index.d.mts +135 -7
- package/dist/index.mjs +194 -20
- package/dist/index.mjs.map +1 -1
- package/dist/pwned.cjs +84 -0
- package/dist/pwned.cjs.map +1 -0
- package/dist/pwned.d.cts +29 -0
- package/dist/pwned.d.mts +29 -0
- package/dist/pwned.mjs +59 -0
- package/dist/pwned.mjs.map +1 -0
- package/dist/server.cjs +223 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +93 -0
- package/dist/server.d.mts +93 -0
- package/dist/server.mjs +193 -0
- package/dist/server.mjs.map +1 -0
- package/dist/strength.cjs +209 -0
- package/dist/strength.cjs.map +1 -0
- package/dist/strength.d.cts +77 -0
- package/dist/strength.d.mts +77 -0
- package/dist/strength.mjs +180 -0
- package/dist/strength.mjs.map +1 -0
- package/package.json +137 -21
- package/dist/index.d.ts +0 -73
- package/dist/index.js +0 -146
- package/dist/index.js.map +0 -1
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates the input type based on visibility.
|
|
5
|
+
*/
|
|
6
|
+
declare const getInputType: (isVisible: boolean) => "text" | "password";
|
|
7
|
+
/**
|
|
8
|
+
* Generates ARIA attributes for the toggle button.
|
|
9
|
+
*/
|
|
10
|
+
declare const getButtonAriaAttributes: (isVisible: boolean, inputId: string, label?: string) => {
|
|
11
|
+
'aria-label': string;
|
|
12
|
+
'aria-pressed': boolean;
|
|
13
|
+
'aria-controls': string;
|
|
14
|
+
type: "button";
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Generates ARIA attributes for the input field.
|
|
18
|
+
*/
|
|
19
|
+
interface InputAriaAttributes {
|
|
20
|
+
'aria-invalid'?: boolean;
|
|
21
|
+
'aria-describedby'?: string;
|
|
22
|
+
}
|
|
23
|
+
declare const getInputAriaAttributes: (isInvalid?: boolean, describedBy?: string) => InputAriaAttributes;
|
|
24
|
+
|
|
25
|
+
/** Error keys. Map them to messages in your own i18n. */
|
|
26
|
+
type PasswordIssue = "password.tooShort" | "password.tooLong" | "password.noLowercase" | "password.noUppercase" | "password.noDigit" | "password.noSymbol" | "password.common" | "password.blocked" | "password.repetitive" | "password.containsPersonalInfo" | "password.mismatch";
|
|
27
|
+
type StrengthScore = 0 | 1 | 2 | 3 | 4;
|
|
28
|
+
type StrengthLevel = "weak" | "fair" | "good" | "strong";
|
|
29
|
+
interface PasswordChecks {
|
|
30
|
+
minLength: boolean;
|
|
31
|
+
maxLength: boolean;
|
|
32
|
+
notCommon: boolean;
|
|
33
|
+
notBlocked: boolean;
|
|
34
|
+
notRepetitive: boolean;
|
|
35
|
+
notPersonal: boolean;
|
|
36
|
+
/** Only present when the policy requires the character class. */
|
|
37
|
+
lowercase?: boolean;
|
|
38
|
+
uppercase?: boolean;
|
|
39
|
+
digit?: boolean;
|
|
40
|
+
symbol?: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface StrengthResult {
|
|
43
|
+
/** 0–1 fail the policy, 2–4 pass it. */
|
|
44
|
+
score: StrengthScore;
|
|
45
|
+
level: StrengthLevel;
|
|
46
|
+
/** Characters (Unicode code points) after NFKC normalization, without zero-width characters. */
|
|
47
|
+
length: number;
|
|
48
|
+
/** No issues. */
|
|
49
|
+
valid: boolean;
|
|
50
|
+
checks: PasswordChecks;
|
|
51
|
+
issues: PasswordIssue[];
|
|
52
|
+
}
|
|
53
|
+
interface EvaluateOptions {
|
|
54
|
+
/** Values the password must not contain, e.g. the user's email and name. Non-strings are ignored. */
|
|
55
|
+
forbidden?: readonly unknown[];
|
|
56
|
+
}
|
|
57
|
+
interface CharacterRequirements {
|
|
58
|
+
lowercase?: boolean;
|
|
59
|
+
uppercase?: boolean;
|
|
60
|
+
digit?: boolean;
|
|
61
|
+
symbol?: boolean;
|
|
62
|
+
}
|
|
63
|
+
type PolicyPreset = "nist" | "legacy";
|
|
64
|
+
interface Policy {
|
|
65
|
+
readonly preset: PolicyPreset;
|
|
66
|
+
readonly minLength: number;
|
|
67
|
+
readonly maxLength: number;
|
|
68
|
+
readonly require: Readonly<Required<CharacterRequirements>>;
|
|
69
|
+
readonly blockCommon: boolean;
|
|
70
|
+
readonly blockRepetitive: boolean;
|
|
71
|
+
readonly blocklist: readonly string[];
|
|
72
|
+
readonly minTokenLength: number;
|
|
73
|
+
/** The engine travels with the policy, so the hook does not import it. */
|
|
74
|
+
readonly evaluate: (password: string, options?: EvaluateOptions) => StrengthResult;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type ViraPasswordInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
78
|
+
ref?: React.Ref<HTMLInputElement>;
|
|
79
|
+
};
|
|
80
|
+
interface UseViraPasswordOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Initial visibility state of the password.
|
|
83
|
+
* @default false
|
|
84
|
+
*/
|
|
85
|
+
defaultVisible?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Controlled visibility. Pair it with `onVisibleChange`.
|
|
88
|
+
*/
|
|
89
|
+
visible?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Called with the next visibility whenever it changes, including automatic hiding.
|
|
92
|
+
*/
|
|
93
|
+
onVisibleChange?: (visible: boolean) => void;
|
|
94
|
+
/**
|
|
95
|
+
* Hide the password when its form is submitted or reset, so password managers see a password field.
|
|
96
|
+
* @default true
|
|
97
|
+
*/
|
|
98
|
+
hideOnSubmit?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Hide the password when focus leaves the input and the toggle, or the browser tab is hidden.
|
|
101
|
+
* @default false
|
|
102
|
+
*/
|
|
103
|
+
hideOnBlur?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* `autoComplete` for the input. `false` leaves the attribute out.
|
|
106
|
+
* @default "new-password" with `policy` or `matchWith`, otherwise "current-password"
|
|
107
|
+
*/
|
|
108
|
+
autoComplete?: string | false;
|
|
109
|
+
/**
|
|
110
|
+
* A policy from `definePolicy()` in `@virastack/password/strength`. Enables `strength` and `issues`.
|
|
111
|
+
*/
|
|
112
|
+
policy?: Policy;
|
|
113
|
+
/**
|
|
114
|
+
* Values the password must not contain, e.g. the user's email and name.
|
|
115
|
+
*/
|
|
116
|
+
forbidden?: readonly unknown[];
|
|
117
|
+
/**
|
|
118
|
+
* The password this field must repeat, e.g. `watch("password")` on a confirm field.
|
|
119
|
+
* Enables `matches` and the `password.mismatch` key.
|
|
120
|
+
*/
|
|
121
|
+
matchWith?: string;
|
|
122
|
+
/**
|
|
123
|
+
* The current value, e.g. `watch("password")`. Without it the hook follows typing in the input,
|
|
124
|
+
* which misses `reset(values)` and `setValue()` from a form library.
|
|
125
|
+
*/
|
|
126
|
+
value?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Custom icons for show/hide states.
|
|
129
|
+
*/
|
|
130
|
+
icons?: {
|
|
131
|
+
show: ReactNode;
|
|
132
|
+
hide: ReactNode;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Whether the input is disabled.
|
|
136
|
+
* If true, the toggle button will also be disabled.
|
|
137
|
+
*/
|
|
138
|
+
disabled?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Whether the input is read-only.
|
|
141
|
+
* If true, the toggle button will be disabled.
|
|
142
|
+
*/
|
|
143
|
+
readOnly?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* ID for the input element. If not provided, a random ID will be generated.
|
|
146
|
+
*/
|
|
147
|
+
id?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Props to be merged with the generated input props, e.g. `register("password")`.
|
|
150
|
+
*/
|
|
151
|
+
inputProps?: ViraPasswordInputProps;
|
|
152
|
+
/**
|
|
153
|
+
* Props to be merged with the generated button props.
|
|
154
|
+
*/
|
|
155
|
+
btnProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
156
|
+
}
|
|
157
|
+
interface UseViraPasswordResult {
|
|
158
|
+
inputProps: ViraPasswordInputProps;
|
|
159
|
+
btnProps: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
160
|
+
isVisible: boolean;
|
|
161
|
+
/** Caps Lock is on while the input has focus. */
|
|
162
|
+
capsLock: boolean;
|
|
163
|
+
toggle: () => void;
|
|
164
|
+
setVisible: (visible: boolean) => void;
|
|
165
|
+
/** Result of the policy for the current value; `null` without a policy. */
|
|
166
|
+
strength: StrengthResult | null;
|
|
167
|
+
/** Spread on the element that shows the strength, so screen readers announce it. */
|
|
168
|
+
strengthProps: ViraPasswordStrengthProps;
|
|
169
|
+
/** With `matchWith`: whether the value repeats it; `null` while the field is empty or without `matchWith`. */
|
|
170
|
+
matches: boolean | null;
|
|
171
|
+
/** Error keys for the current value. */
|
|
172
|
+
issues: PasswordIssue[];
|
|
173
|
+
/** A validation rule for React Hook Form: `register("password", pw.rule)`. Returns the first error key. */
|
|
174
|
+
rule: ViraPasswordRule;
|
|
175
|
+
}
|
|
176
|
+
interface ViraPasswordStrengthProps {
|
|
177
|
+
id: string;
|
|
178
|
+
role: 'status';
|
|
179
|
+
'aria-live': 'polite';
|
|
180
|
+
'aria-atomic': true;
|
|
181
|
+
}
|
|
182
|
+
interface ViraPasswordRule {
|
|
183
|
+
validate: (value: unknown) => true | PasswordIssue;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Combines refs into one callback ref. Supports React 19 ref cleanup functions.
|
|
187
|
+
*/
|
|
188
|
+
declare const composeRefs: <T>(...refs: (React.Ref<T> | undefined)[]) => React.RefCallback<T>;
|
|
189
|
+
/**
|
|
190
|
+
* A utility to merge event handlers, class names, `aria-describedby` and refs.
|
|
191
|
+
* Later props win; handlers run in order.
|
|
192
|
+
*/
|
|
193
|
+
declare function mergeProps<T extends object>(...args: (T | undefined)[]): T;
|
|
194
|
+
declare function mergeProps<A extends object, B extends object>(a: A | undefined, b: B | undefined): Omit<A, keyof B> & B;
|
|
195
|
+
declare const useViraPassword: (options?: UseViraPasswordOptions) => UseViraPasswordResult;
|
|
196
|
+
|
|
197
|
+
declare const EyeIcon: (props: React.SVGProps<SVGSVGElement>) => React.ReactSVGElement;
|
|
198
|
+
declare const EyeOffIcon: (props: React.SVGProps<SVGSVGElement>) => React.ReactSVGElement;
|
|
199
|
+
|
|
200
|
+
export { EyeIcon, EyeOffIcon, composeRefs, getButtonAriaAttributes, getInputAriaAttributes, getInputType, mergeProps, useViraPassword };
|
|
201
|
+
export type { InputAriaAttributes, UseViraPasswordOptions, UseViraPasswordResult, ViraPasswordInputProps, ViraPasswordRule, ViraPasswordStrengthProps };
|
package/dist/index.d.mts
CHANGED
|
@@ -16,14 +16,114 @@ declare const getButtonAriaAttributes: (isVisible: boolean, inputId: string, lab
|
|
|
16
16
|
/**
|
|
17
17
|
* Generates ARIA attributes for the input field.
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
interface InputAriaAttributes {
|
|
20
|
+
'aria-invalid'?: boolean;
|
|
21
|
+
'aria-describedby'?: string;
|
|
22
|
+
}
|
|
23
|
+
declare const getInputAriaAttributes: (isInvalid?: boolean, describedBy?: string) => InputAriaAttributes;
|
|
24
|
+
|
|
25
|
+
/** Error keys. Map them to messages in your own i18n. */
|
|
26
|
+
type PasswordIssue = "password.tooShort" | "password.tooLong" | "password.noLowercase" | "password.noUppercase" | "password.noDigit" | "password.noSymbol" | "password.common" | "password.blocked" | "password.repetitive" | "password.containsPersonalInfo" | "password.mismatch";
|
|
27
|
+
type StrengthScore = 0 | 1 | 2 | 3 | 4;
|
|
28
|
+
type StrengthLevel = "weak" | "fair" | "good" | "strong";
|
|
29
|
+
interface PasswordChecks {
|
|
30
|
+
minLength: boolean;
|
|
31
|
+
maxLength: boolean;
|
|
32
|
+
notCommon: boolean;
|
|
33
|
+
notBlocked: boolean;
|
|
34
|
+
notRepetitive: boolean;
|
|
35
|
+
notPersonal: boolean;
|
|
36
|
+
/** Only present when the policy requires the character class. */
|
|
37
|
+
lowercase?: boolean;
|
|
38
|
+
uppercase?: boolean;
|
|
39
|
+
digit?: boolean;
|
|
40
|
+
symbol?: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface StrengthResult {
|
|
43
|
+
/** 0–1 fail the policy, 2–4 pass it. */
|
|
44
|
+
score: StrengthScore;
|
|
45
|
+
level: StrengthLevel;
|
|
46
|
+
/** Characters (Unicode code points) after NFKC normalization, without zero-width characters. */
|
|
47
|
+
length: number;
|
|
48
|
+
/** No issues. */
|
|
49
|
+
valid: boolean;
|
|
50
|
+
checks: PasswordChecks;
|
|
51
|
+
issues: PasswordIssue[];
|
|
52
|
+
}
|
|
53
|
+
interface EvaluateOptions {
|
|
54
|
+
/** Values the password must not contain, e.g. the user's email and name. Non-strings are ignored. */
|
|
55
|
+
forbidden?: readonly unknown[];
|
|
56
|
+
}
|
|
57
|
+
interface CharacterRequirements {
|
|
58
|
+
lowercase?: boolean;
|
|
59
|
+
uppercase?: boolean;
|
|
60
|
+
digit?: boolean;
|
|
61
|
+
symbol?: boolean;
|
|
62
|
+
}
|
|
63
|
+
type PolicyPreset = "nist" | "legacy";
|
|
64
|
+
interface Policy {
|
|
65
|
+
readonly preset: PolicyPreset;
|
|
66
|
+
readonly minLength: number;
|
|
67
|
+
readonly maxLength: number;
|
|
68
|
+
readonly require: Readonly<Required<CharacterRequirements>>;
|
|
69
|
+
readonly blockCommon: boolean;
|
|
70
|
+
readonly blockRepetitive: boolean;
|
|
71
|
+
readonly blocklist: readonly string[];
|
|
72
|
+
readonly minTokenLength: number;
|
|
73
|
+
/** The engine travels with the policy, so the hook does not import it. */
|
|
74
|
+
readonly evaluate: (password: string, options?: EvaluateOptions) => StrengthResult;
|
|
75
|
+
}
|
|
20
76
|
|
|
77
|
+
type ViraPasswordInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
78
|
+
ref?: React.Ref<HTMLInputElement>;
|
|
79
|
+
};
|
|
21
80
|
interface UseViraPasswordOptions {
|
|
22
81
|
/**
|
|
23
82
|
* Initial visibility state of the password.
|
|
24
83
|
* @default false
|
|
25
84
|
*/
|
|
26
85
|
defaultVisible?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Controlled visibility. Pair it with `onVisibleChange`.
|
|
88
|
+
*/
|
|
89
|
+
visible?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Called with the next visibility whenever it changes, including automatic hiding.
|
|
92
|
+
*/
|
|
93
|
+
onVisibleChange?: (visible: boolean) => void;
|
|
94
|
+
/**
|
|
95
|
+
* Hide the password when its form is submitted or reset, so password managers see a password field.
|
|
96
|
+
* @default true
|
|
97
|
+
*/
|
|
98
|
+
hideOnSubmit?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Hide the password when focus leaves the input and the toggle, or the browser tab is hidden.
|
|
101
|
+
* @default false
|
|
102
|
+
*/
|
|
103
|
+
hideOnBlur?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* `autoComplete` for the input. `false` leaves the attribute out.
|
|
106
|
+
* @default "new-password" with `policy` or `matchWith`, otherwise "current-password"
|
|
107
|
+
*/
|
|
108
|
+
autoComplete?: string | false;
|
|
109
|
+
/**
|
|
110
|
+
* A policy from `definePolicy()` in `@virastack/password/strength`. Enables `strength` and `issues`.
|
|
111
|
+
*/
|
|
112
|
+
policy?: Policy;
|
|
113
|
+
/**
|
|
114
|
+
* Values the password must not contain, e.g. the user's email and name.
|
|
115
|
+
*/
|
|
116
|
+
forbidden?: readonly unknown[];
|
|
117
|
+
/**
|
|
118
|
+
* The password this field must repeat, e.g. `watch("password")` on a confirm field.
|
|
119
|
+
* Enables `matches` and the `password.mismatch` key.
|
|
120
|
+
*/
|
|
121
|
+
matchWith?: string;
|
|
122
|
+
/**
|
|
123
|
+
* The current value, e.g. `watch("password")`. Without it the hook follows typing in the input,
|
|
124
|
+
* which misses `reset(values)` and `setValue()` from a form library.
|
|
125
|
+
*/
|
|
126
|
+
value?: string;
|
|
27
127
|
/**
|
|
28
128
|
* Custom icons for show/hide states.
|
|
29
129
|
*/
|
|
@@ -46,28 +146,56 @@ interface UseViraPasswordOptions {
|
|
|
46
146
|
*/
|
|
47
147
|
id?: string;
|
|
48
148
|
/**
|
|
49
|
-
* Props to be merged with the generated input props.
|
|
149
|
+
* Props to be merged with the generated input props, e.g. `register("password")`.
|
|
50
150
|
*/
|
|
51
|
-
inputProps?:
|
|
151
|
+
inputProps?: ViraPasswordInputProps;
|
|
52
152
|
/**
|
|
53
153
|
* Props to be merged with the generated button props.
|
|
54
154
|
*/
|
|
55
155
|
btnProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
56
156
|
}
|
|
57
157
|
interface UseViraPasswordResult {
|
|
58
|
-
inputProps:
|
|
158
|
+
inputProps: ViraPasswordInputProps;
|
|
59
159
|
btnProps: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
60
160
|
isVisible: boolean;
|
|
161
|
+
/** Caps Lock is on while the input has focus. */
|
|
162
|
+
capsLock: boolean;
|
|
61
163
|
toggle: () => void;
|
|
62
164
|
setVisible: (visible: boolean) => void;
|
|
165
|
+
/** Result of the policy for the current value; `null` without a policy. */
|
|
166
|
+
strength: StrengthResult | null;
|
|
167
|
+
/** Spread on the element that shows the strength, so screen readers announce it. */
|
|
168
|
+
strengthProps: ViraPasswordStrengthProps;
|
|
169
|
+
/** With `matchWith`: whether the value repeats it; `null` while the field is empty or without `matchWith`. */
|
|
170
|
+
matches: boolean | null;
|
|
171
|
+
/** Error keys for the current value. */
|
|
172
|
+
issues: PasswordIssue[];
|
|
173
|
+
/** A validation rule for React Hook Form: `register("password", pw.rule)`. Returns the first error key. */
|
|
174
|
+
rule: ViraPasswordRule;
|
|
175
|
+
}
|
|
176
|
+
interface ViraPasswordStrengthProps {
|
|
177
|
+
id: string;
|
|
178
|
+
role: 'status';
|
|
179
|
+
'aria-live': 'polite';
|
|
180
|
+
'aria-atomic': true;
|
|
63
181
|
}
|
|
182
|
+
interface ViraPasswordRule {
|
|
183
|
+
validate: (value: unknown) => true | PasswordIssue;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Combines refs into one callback ref. Supports React 19 ref cleanup functions.
|
|
187
|
+
*/
|
|
188
|
+
declare const composeRefs: <T>(...refs: (React.Ref<T> | undefined)[]) => React.RefCallback<T>;
|
|
64
189
|
/**
|
|
65
|
-
* A utility to merge event handlers and
|
|
190
|
+
* A utility to merge event handlers, class names, `aria-describedby` and refs.
|
|
191
|
+
* Later props win; handlers run in order.
|
|
66
192
|
*/
|
|
67
|
-
declare
|
|
193
|
+
declare function mergeProps<T extends object>(...args: (T | undefined)[]): T;
|
|
194
|
+
declare function mergeProps<A extends object, B extends object>(a: A | undefined, b: B | undefined): Omit<A, keyof B> & B;
|
|
68
195
|
declare const useViraPassword: (options?: UseViraPasswordOptions) => UseViraPasswordResult;
|
|
69
196
|
|
|
70
197
|
declare const EyeIcon: (props: React.SVGProps<SVGSVGElement>) => React.ReactSVGElement;
|
|
71
198
|
declare const EyeOffIcon: (props: React.SVGProps<SVGSVGElement>) => React.ReactSVGElement;
|
|
72
199
|
|
|
73
|
-
export { EyeIcon, EyeOffIcon,
|
|
200
|
+
export { EyeIcon, EyeOffIcon, composeRefs, getButtonAriaAttributes, getInputAriaAttributes, getInputType, mergeProps, useViraPassword };
|
|
201
|
+
export type { InputAriaAttributes, UseViraPasswordOptions, UseViraPasswordResult, ViraPasswordInputProps, ViraPasswordRule, ViraPasswordStrengthProps };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import React, { useState, useId, useCallback } from 'react';
|
|
3
2
|
|
|
4
3
|
// src/core/logic.ts
|
|
5
4
|
var getInputType = (isVisible) => {
|
|
@@ -23,6 +22,13 @@ var getInputAriaAttributes = (isInvalid, describedBy) => {
|
|
|
23
22
|
}
|
|
24
23
|
return attrs;
|
|
25
24
|
};
|
|
25
|
+
|
|
26
|
+
// src/react/useViraPassword.ts
|
|
27
|
+
import React2, { useState, useId, useCallback, useEffect, useRef } from "react";
|
|
28
|
+
import { flushSync } from "react-dom";
|
|
29
|
+
|
|
30
|
+
// src/icons/index.ts
|
|
31
|
+
import React from "react";
|
|
26
32
|
var EyeIcon = (props) => React.createElement(
|
|
27
33
|
"svg",
|
|
28
34
|
{
|
|
@@ -61,30 +67,56 @@ var EyeOffIcon = (props) => React.createElement(
|
|
|
61
67
|
);
|
|
62
68
|
|
|
63
69
|
// src/react/useViraPassword.ts
|
|
64
|
-
var
|
|
70
|
+
var setRef = (ref, value) => {
|
|
71
|
+
if (typeof ref === "function") return ref(value);
|
|
72
|
+
if (ref) ref.current = value;
|
|
73
|
+
};
|
|
74
|
+
var composeRefs = (...refs) => (node) => {
|
|
75
|
+
const cleanups = refs.map((ref) => setRef(ref, node));
|
|
76
|
+
if (!cleanups.some((cleanup) => typeof cleanup === "function")) return;
|
|
77
|
+
return () => {
|
|
78
|
+
refs.forEach((ref, i) => {
|
|
79
|
+
const cleanup = cleanups[i];
|
|
80
|
+
if (typeof cleanup === "function") cleanup();
|
|
81
|
+
else setRef(ref, null);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
function mergeProps(...args) {
|
|
65
86
|
const result = {};
|
|
66
87
|
for (const props of args) {
|
|
67
88
|
if (!props) continue;
|
|
68
|
-
for (const key
|
|
69
|
-
const val = props[key];
|
|
89
|
+
for (const [key, val] of Object.entries(props)) {
|
|
70
90
|
const existing = result[key];
|
|
71
91
|
if (key.startsWith("on") && typeof val === "function" && typeof existing === "function") {
|
|
72
92
|
result[key] = (...args2) => {
|
|
73
93
|
existing(...args2);
|
|
74
94
|
val(...args2);
|
|
75
95
|
};
|
|
76
|
-
} else if (key === "className" && typeof val === "string" && typeof existing === "string") {
|
|
96
|
+
} else if ((key === "className" || key === "aria-describedby") && typeof val === "string" && typeof existing === "string") {
|
|
77
97
|
result[key] = [existing, val].filter(Boolean).join(" ");
|
|
98
|
+
} else if (key === "ref" && existing && val) {
|
|
99
|
+
result[key] = composeRefs(existing, val);
|
|
78
100
|
} else {
|
|
79
101
|
result[key] = val;
|
|
80
102
|
}
|
|
81
103
|
}
|
|
82
104
|
}
|
|
83
105
|
return result;
|
|
84
|
-
}
|
|
106
|
+
}
|
|
107
|
+
var sameText = (a, b) => a.normalize("NFKC") === b.normalize("NFKC");
|
|
85
108
|
var useViraPassword = (options = {}) => {
|
|
86
109
|
const {
|
|
87
110
|
defaultVisible = false,
|
|
111
|
+
visible: controlledVisible,
|
|
112
|
+
onVisibleChange,
|
|
113
|
+
hideOnSubmit = true,
|
|
114
|
+
hideOnBlur = false,
|
|
115
|
+
autoComplete,
|
|
116
|
+
policy,
|
|
117
|
+
forbidden,
|
|
118
|
+
matchWith,
|
|
119
|
+
value: providedValue,
|
|
88
120
|
icons,
|
|
89
121
|
disabled = false,
|
|
90
122
|
readOnly = false,
|
|
@@ -92,28 +124,157 @@ var useViraPassword = (options = {}) => {
|
|
|
92
124
|
inputProps: userInputProps,
|
|
93
125
|
btnProps: userBtnProps
|
|
94
126
|
} = options;
|
|
95
|
-
const [
|
|
127
|
+
const [uncontrolledVisible, setUncontrolledVisible] = useState(defaultVisible);
|
|
128
|
+
const [capsLock, setCapsLock] = useState(false);
|
|
129
|
+
const userValue = userInputProps?.value;
|
|
130
|
+
const checksValue = policy !== void 0 || matchWith !== void 0;
|
|
131
|
+
const tracksValue = checksValue && providedValue === void 0 && typeof userValue !== "string";
|
|
132
|
+
const [typed, setTyped] = useState(() => {
|
|
133
|
+
const initial = userInputProps?.defaultValue;
|
|
134
|
+
return typeof initial === "string" ? initial : "";
|
|
135
|
+
});
|
|
136
|
+
const value = providedValue ?? (typeof userValue === "string" ? userValue : typed);
|
|
137
|
+
const isVisible = controlledVisible ?? uncontrolledVisible;
|
|
96
138
|
const generatedId = useId();
|
|
97
|
-
const inputId = providedId || `virapassword-input-${generatedId}`;
|
|
139
|
+
const inputId = typeof userInputProps?.id === "string" && userInputProps.id || providedId || `virapassword-input-${generatedId}`;
|
|
140
|
+
const latest = useRef({ isVisible, controlled: controlledVisible !== void 0, onVisibleChange, policy, forbidden, matchWith, hideOnBlur, tracksValue });
|
|
141
|
+
latest.current = { isVisible, controlled: controlledVisible !== void 0, onVisibleChange, policy, forbidden, matchWith, hideOnBlur, tracksValue };
|
|
142
|
+
const pointerPress = useRef(null);
|
|
143
|
+
const getInput = useCallback(() => {
|
|
144
|
+
const element = typeof document === "undefined" ? null : document.getElementById(inputId);
|
|
145
|
+
return element instanceof HTMLInputElement ? element : null;
|
|
146
|
+
}, [inputId]);
|
|
147
|
+
const commit = useCallback((next) => {
|
|
148
|
+
const current = latest.current;
|
|
149
|
+
if (next === current.isVisible) return;
|
|
150
|
+
if (!current.controlled) setUncontrolledVisible(next);
|
|
151
|
+
current.onVisibleChange?.(next);
|
|
152
|
+
}, []);
|
|
98
153
|
const toggle = useCallback(() => {
|
|
99
154
|
if (disabled || readOnly) return;
|
|
100
|
-
|
|
101
|
-
}, [disabled, readOnly]);
|
|
155
|
+
commit(!latest.current.isVisible);
|
|
156
|
+
}, [disabled, readOnly, commit]);
|
|
102
157
|
const setVisible = useCallback((visible) => {
|
|
103
158
|
if (disabled || readOnly) return;
|
|
104
|
-
|
|
105
|
-
}, [disabled, readOnly]);
|
|
106
|
-
const
|
|
107
|
-
|
|
159
|
+
commit(visible);
|
|
160
|
+
}, [disabled, readOnly, commit]);
|
|
161
|
+
const hide = useCallback(() => commit(false), [commit]);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!hideOnSubmit) return;
|
|
164
|
+
const isOwnForm = (event) => {
|
|
165
|
+
const form = getInput()?.form;
|
|
166
|
+
return form != null && event.target === form;
|
|
167
|
+
};
|
|
168
|
+
const onSubmit = (event) => {
|
|
169
|
+
if (isOwnForm(event) && latest.current.isVisible) flushSync(hide);
|
|
170
|
+
};
|
|
171
|
+
const onReset = (event) => {
|
|
172
|
+
if (isOwnForm(event) && !event.defaultPrevented) hide();
|
|
173
|
+
};
|
|
174
|
+
document.addEventListener("submit", onSubmit, true);
|
|
175
|
+
document.addEventListener("reset", onReset);
|
|
176
|
+
return () => {
|
|
177
|
+
document.removeEventListener("submit", onSubmit, true);
|
|
178
|
+
document.removeEventListener("reset", onReset);
|
|
179
|
+
};
|
|
180
|
+
}, [hideOnSubmit, getInput, hide]);
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!tracksValue) return;
|
|
183
|
+
const onReset = (event) => {
|
|
184
|
+
const input = getInput();
|
|
185
|
+
if (input?.form && event.target === input.form) setTimeout(() => setTyped(input.value));
|
|
186
|
+
};
|
|
187
|
+
document.addEventListener("reset", onReset);
|
|
188
|
+
return () => document.removeEventListener("reset", onReset);
|
|
189
|
+
}, [tracksValue, getInput]);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!hideOnBlur) return;
|
|
192
|
+
const onVisibilityChange = () => {
|
|
193
|
+
if (document.visibilityState === "hidden") hide();
|
|
194
|
+
};
|
|
195
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
196
|
+
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
197
|
+
}, [hideOnBlur, hide]);
|
|
198
|
+
const hideOnFocusLeave = useCallback((next) => {
|
|
199
|
+
if (!latest.current.hideOnBlur || pointerPress.current) return;
|
|
200
|
+
if (next instanceof Element && (next.id === inputId || next.getAttribute("aria-controls") === inputId)) return;
|
|
201
|
+
hide();
|
|
202
|
+
}, [inputId, hide]);
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const isInput = (target) => target instanceof HTMLInputElement && target.id === inputId;
|
|
205
|
+
const onKey = (event) => {
|
|
206
|
+
if (isInput(event.target)) setCapsLock(event.getModifierState?.("CapsLock") ?? false);
|
|
207
|
+
};
|
|
208
|
+
const onFocusOut = (event) => {
|
|
209
|
+
if (!isInput(event.target)) return;
|
|
210
|
+
setCapsLock(false);
|
|
211
|
+
hideOnFocusLeave(event.relatedTarget);
|
|
212
|
+
};
|
|
213
|
+
const onInput = (event) => {
|
|
214
|
+
if (latest.current.tracksValue && isInput(event.target)) setTyped(event.target.value);
|
|
215
|
+
};
|
|
216
|
+
document.addEventListener("keydown", onKey, true);
|
|
217
|
+
document.addEventListener("keyup", onKey, true);
|
|
218
|
+
document.addEventListener("focusout", onFocusOut, true);
|
|
219
|
+
document.addEventListener("input", onInput, true);
|
|
220
|
+
return () => {
|
|
221
|
+
document.removeEventListener("keydown", onKey, true);
|
|
222
|
+
document.removeEventListener("keyup", onKey, true);
|
|
223
|
+
document.removeEventListener("focusout", onFocusOut, true);
|
|
224
|
+
document.removeEventListener("input", onInput, true);
|
|
225
|
+
};
|
|
226
|
+
}, [inputId, hideOnFocusLeave]);
|
|
227
|
+
const strength = policy ? policy.evaluate(value, { forbidden }) : null;
|
|
228
|
+
const matches = matchWith === void 0 || value === "" ? null : sameText(value, matchWith);
|
|
229
|
+
const issues = strength ? [...strength.issues] : [];
|
|
230
|
+
if (matches === false) issues.push("password.mismatch");
|
|
231
|
+
const strengthId = `${inputId}-strength`;
|
|
232
|
+
const [rule] = useState(() => ({
|
|
233
|
+
validate: (input) => {
|
|
234
|
+
const { policy: currentPolicy, forbidden: currentForbidden, matchWith: currentMatch } = latest.current;
|
|
235
|
+
const text = typeof input === "string" ? input : "";
|
|
236
|
+
const result = currentPolicy?.evaluate(text, { forbidden: currentForbidden });
|
|
237
|
+
if (result && !result.valid) return result.issues[0];
|
|
238
|
+
if (currentMatch !== void 0 && !sameText(text, currentMatch)) return "password.mismatch";
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}));
|
|
242
|
+
const showIcon = icons?.show ?? React2.createElement(EyeIcon);
|
|
243
|
+
const hideIcon = icons?.hide ?? React2.createElement(EyeOffIcon);
|
|
108
244
|
const baseInputProps = {
|
|
109
245
|
id: inputId,
|
|
110
246
|
type: getInputType(isVisible),
|
|
111
247
|
disabled,
|
|
112
248
|
readOnly,
|
|
113
|
-
...
|
|
249
|
+
...autoComplete === false ? {} : { autoComplete: autoComplete ?? (checksValue ? "new-password" : "current-password") },
|
|
250
|
+
spellCheck: false,
|
|
251
|
+
autoCorrect: "off",
|
|
252
|
+
autoCapitalize: "off",
|
|
253
|
+
...getInputAriaAttributes(void 0, policy ? strengthId : void 0)
|
|
114
254
|
};
|
|
115
255
|
const baseBtnProps = {
|
|
116
|
-
|
|
256
|
+
onPointerDown: () => {
|
|
257
|
+
const input = getInput();
|
|
258
|
+
const press = { start: input?.selectionStart ?? null, end: input?.selectionEnd ?? null };
|
|
259
|
+
pointerPress.current = press;
|
|
260
|
+
window.addEventListener("pointerup", () => setTimeout(() => {
|
|
261
|
+
if (pointerPress.current === press) pointerPress.current = null;
|
|
262
|
+
}), { once: true });
|
|
263
|
+
},
|
|
264
|
+
onPointerCancel: () => {
|
|
265
|
+
pointerPress.current = null;
|
|
266
|
+
},
|
|
267
|
+
onClick: () => {
|
|
268
|
+
const press = pointerPress.current;
|
|
269
|
+
pointerPress.current = null;
|
|
270
|
+
flushSync(toggle);
|
|
271
|
+
if (!press || disabled || readOnly) return;
|
|
272
|
+
const input = getInput();
|
|
273
|
+
if (!input) return;
|
|
274
|
+
input.focus();
|
|
275
|
+
if (press.start !== null && press.end !== null) input.setSelectionRange(press.start, press.end);
|
|
276
|
+
},
|
|
277
|
+
onBlur: (event) => hideOnFocusLeave(event.relatedTarget),
|
|
117
278
|
disabled: disabled || readOnly,
|
|
118
279
|
children: isVisible ? hideIcon : showIcon,
|
|
119
280
|
...getButtonAriaAttributes(isVisible, inputId)
|
|
@@ -124,11 +285,24 @@ var useViraPassword = (options = {}) => {
|
|
|
124
285
|
inputProps: mergedInputProps,
|
|
125
286
|
btnProps: mergedBtnProps,
|
|
126
287
|
isVisible,
|
|
288
|
+
capsLock,
|
|
127
289
|
toggle,
|
|
128
|
-
setVisible
|
|
290
|
+
setVisible,
|
|
291
|
+
strength,
|
|
292
|
+
matches,
|
|
293
|
+
strengthProps: { id: strengthId, role: "status", "aria-live": "polite", "aria-atomic": true },
|
|
294
|
+
issues,
|
|
295
|
+
rule
|
|
129
296
|
};
|
|
130
297
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
298
|
+
export {
|
|
299
|
+
EyeIcon,
|
|
300
|
+
EyeOffIcon,
|
|
301
|
+
composeRefs,
|
|
302
|
+
getButtonAriaAttributes,
|
|
303
|
+
getInputAriaAttributes,
|
|
304
|
+
getInputType,
|
|
305
|
+
mergeProps,
|
|
306
|
+
useViraPassword
|
|
307
|
+
};
|
|
134
308
|
//# sourceMappingURL=index.mjs.map
|