@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.
@@ -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
- declare const getInputAriaAttributes: (isInvalid?: boolean, describedBy?: string) => Record<string, any>;
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?: React.InputHTMLAttributes<HTMLInputElement>;
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: React.InputHTMLAttributes<HTMLInputElement>;
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 other props.
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 const mergeProps: <T extends Record<string, any>>(...args: (T | undefined)[]) => T;
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, type UseViraPasswordOptions, type UseViraPasswordResult, getButtonAriaAttributes, getInputAriaAttributes, getInputType, mergeProps, useViraPassword };
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 mergeProps = (...args) => {
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 in props) {
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 [isVisible, setIsVisible] = useState(defaultVisible);
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
- setIsVisible((prev) => !prev);
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
- setIsVisible(visible);
105
- }, [disabled, readOnly]);
106
- const showIcon = icons?.show ?? React.createElement(EyeIcon);
107
- const hideIcon = icons?.hide ?? React.createElement(EyeOffIcon);
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
- ...getInputAriaAttributes()
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
- onClick: toggle,
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
- export { EyeIcon, EyeOffIcon, getButtonAriaAttributes, getInputAriaAttributes, getInputType, mergeProps, useViraPassword };
133
- //# sourceMappingURL=index.mjs.map
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