notionsoft-ui 1.0.20 → 1.0.22

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.
@@ -8,8 +8,8 @@ import React, {
8
8
  import { createPortal } from "react-dom";
9
9
  import { buildNestedFiltersQuery, cn, useDebounce } from "../../utils/cn";
10
10
  import Input from "../input";
11
- import { Check, Eraser, ListFilter, X } from "lucide-react";
12
- import CircleLoader from "../circle-loader";
11
+ import { Check, Eraser, List, ListFilter, LoaderCircle, X } from "lucide-react";
12
+ import { NastranInputSize } from "../input/input";
13
13
 
14
14
  export interface FilterItem {
15
15
  key: string;
@@ -25,7 +25,6 @@ export type MultiSelectInputProps<T = any> = Omit<
25
25
  React.InputHTMLAttributes<HTMLInputElement>,
26
26
  "onSelect"
27
27
  > & {
28
- // Either `fetch` function OR `apiConfig` must be provided
29
28
  fetch?: (
30
29
  value: string,
31
30
  filters?: Record<string, boolean>,
@@ -37,20 +36,31 @@ export type MultiSelectInputProps<T = any> = Omit<
37
36
  filters?: FilterItem[];
38
37
  onFiltersChange?: (filtersState: Record<string, boolean>) => void;
39
38
  debounceValue?: number;
40
- parentClassName?: string;
39
+ classNames?: {
40
+ rootDivClassName?: string;
41
+ };
41
42
  text?: {
42
- fetch?: string;
43
43
  notItem?: string;
44
44
  maxRecord?: string;
45
45
  clearFilters?: string;
46
+ required?: string;
47
+ label?: string;
46
48
  };
49
+ fixedOptions?: T[];
50
+ refechDependency?: any[];
51
+ showMaxFetch?: boolean;
47
52
  endContent?: React.ReactNode;
53
+ startContent?: React.ReactNode;
54
+ errorMessage?: string;
48
55
  selectionMode?: "single" | "multiple";
49
56
  selected?: T | T[];
50
57
  onItemsSelect?: (selected: T | T[]) => void;
58
+ onClear?: () => void;
51
59
  searchBy?: keyof T | (keyof T)[];
52
60
  itemKey?: keyof T;
53
61
  STORAGE_KEY?: string;
62
+ measurement?: NastranInputSize;
63
+ readOnly?: boolean;
54
64
  } & (
55
65
  | {
56
66
  fetch: (
@@ -70,14 +80,14 @@ function MultiSelectInputInner<T = any>(
70
80
  filters = [],
71
81
  onFiltersChange,
72
82
  debounceValue = 500,
73
- parentClassName,
83
+ classNames,
74
84
  text = {
75
- fetch: "Fetching...",
76
85
  notItem: "No results found",
77
86
  maxRecord: "Max records",
78
87
  clearFilters: "Clear Filters",
79
88
  },
80
89
  endContent,
90
+ startContent,
81
91
  STORAGE_KEY = "FILTER_STORAGE_KEY",
82
92
  selectionMode,
83
93
  selected,
@@ -85,6 +95,12 @@ function MultiSelectInputInner<T = any>(
85
95
  searchBy,
86
96
  itemKey,
87
97
  apiConfig,
98
+ errorMessage,
99
+ readOnly,
100
+ showMaxFetch,
101
+ fixedOptions,
102
+ refechDependency = [],
103
+ onClear,
88
104
  ...props
89
105
  }: MultiSelectInputProps<T>,
90
106
  ref: React.Ref<HTMLInputElement>
@@ -94,7 +110,10 @@ function MultiSelectInputInner<T = any>(
94
110
  const [showFilters, setShowFilters] = useState(false);
95
111
  const [isFetching, setIsFetching] = useState(false);
96
112
  const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
97
- const [items, setItems] = useState<T[]>([]);
113
+ const [items, setItems] = useState<T[]>(fixedOptions ?? []);
114
+ const [showSelectedOnly, setShowSelectedOnly] = useState(false);
115
+ const [shouldFetch, setShouldFetch] = useState(false);
116
+ const { rootDivClassName } = classNames || {};
98
117
  const [filtersState, setFiltersState] = useState<Record<string, boolean>>(
99
118
  () => {
100
119
  try {
@@ -104,19 +123,47 @@ function MultiSelectInputInner<T = any>(
104
123
  return filters.reduce((acc, f) => ({ ...acc, [f.key]: false }), {});
105
124
  }
106
125
  );
126
+ const [dropDirection, setDropDirection] = useState<"down" | "up">("down");
107
127
 
108
- const [maxFetch, setMaxFetch] = useState<number | "">(() => {
128
+ const [maxFetch, setMaxFetch] = useState<number>(() => {
109
129
  try {
110
130
  const saved = localStorage.getItem(`${STORAGE_KEY}_MAX_FETCH`);
111
- return saved ? Number(saved) : "";
131
+ return saved ? Number(saved) : 30;
112
132
  } catch {
113
- return "";
133
+ return 30;
114
134
  }
115
135
  });
116
136
 
117
137
  const [selectedItems, setSelectedItems] = useState<T[]>(
118
138
  Array.isArray(selected) ? selected : selected ? [selected] : []
119
139
  );
140
+ useEffect(() => {
141
+ const newSelected = Array.isArray(selected)
142
+ ? selected
143
+ : selected
144
+ ? [selected]
145
+ : [];
146
+ const key = itemKey as keyof T;
147
+
148
+ setSelectedItems((prev) => {
149
+ const combinedMap = new Map<string, T>();
150
+
151
+ // Add newSelected from prop first
152
+ newSelected.forEach((item) => {
153
+ const id = key ? String((item as any)[key]) : String(item);
154
+ combinedMap.set(id, item);
155
+ });
156
+
157
+ prev.forEach((item) => {
158
+ const id = key ? String((item as any)[key]) : String(item);
159
+ if (!combinedMap.has(id)) {
160
+ combinedMap.delete(id);
161
+ }
162
+ });
163
+ return Array.from(combinedMap.values());
164
+ });
165
+ }, [selected]);
166
+
120
167
  const [pendingSelection, setPendingSelection] = useState<T[] | T | null>(
121
168
  null
122
169
  );
@@ -126,8 +173,16 @@ function MultiSelectInputInner<T = any>(
126
173
  const dropdownRef = useRef<HTMLDivElement>(null);
127
174
 
128
175
  const debouncedValue = useDebounce(inputValue, debounceValue);
176
+ const fetchRef = useRef(fetch);
129
177
 
130
178
  useEffect(() => {
179
+ fetchRef.current = fetch;
180
+ }, [fetch]);
181
+ useEffect(() => {
182
+ if (shouldFetch) setShouldFetch(false); // Allow fetch when function changes
183
+ }, [maxFetch, ...refechDependency]);
184
+ useEffect(() => {
185
+ if (!shouldFetch || !isFocused || fixedOptions || !fetch) return; // ⛔ skip until first focus
131
186
  const get = async () => {
132
187
  setIsFetching(true);
133
188
  try {
@@ -135,7 +190,7 @@ function MultiSelectInputInner<T = any>(
135
190
 
136
191
  if (fetch) {
137
192
  // User-provided fetch function
138
- data = await fetch(
193
+ data = await fetchRef.current(
139
194
  debouncedValue,
140
195
  filtersState,
141
196
  maxFetch && !isNaN(Number(maxFetch)) ? Number(maxFetch) : undefined
@@ -151,7 +206,7 @@ function MultiSelectInputInner<T = any>(
151
206
 
152
207
  const combinedParams = new URLSearchParams({
153
208
  q: debouncedValue,
154
- maxFetch: maxFetch?.toString() ?? "",
209
+ max: maxFetch?.toString() ?? "",
155
210
  ...apiConfig.params,
156
211
  }).toString();
157
212
 
@@ -175,18 +230,46 @@ function MultiSelectInputInner<T = any>(
175
230
  };
176
231
 
177
232
  get();
178
- }, [debouncedValue, fetch, apiConfig, filtersState, maxFetch]);
179
-
233
+ }, [debouncedValue, filtersState, maxFetch, shouldFetch]);
234
+ useLayoutEffect(() => {
235
+ if (dropdownRef.current) {
236
+ updatePosition();
237
+ }
238
+ }, [items, showSelectedOnly]);
180
239
  // Update dropdown position
181
240
  const updatePosition = () => {
182
- const el = containerRef.current;
183
- if (!el) return;
184
- const rect = el.getBoundingClientRect();
185
- setPosition({
186
- top: rect.bottom + window.scrollY,
187
- left: rect.left + window.scrollX,
188
- width: rect.width,
189
- });
241
+ const inputEl = containerRef.current;
242
+ const dropdownEl = dropdownRef.current;
243
+ if (!inputEl || !dropdownEl) return;
244
+
245
+ const rect = inputEl.getBoundingClientRect();
246
+ const viewportHeight = window.innerHeight;
247
+ const gap = 4; // distance between input and dropdown
248
+
249
+ // Actual dropdown height based on content, capped at 260px
250
+ const dropdownHeight = Math.min(dropdownEl.offsetHeight || 0, 260);
251
+
252
+ const spaceBelow = viewportHeight - rect.bottom;
253
+ const spaceAbove = rect.top;
254
+
255
+ // Decide direction
256
+ if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
257
+ // Flip above
258
+ setDropDirection("up");
259
+ setPosition({
260
+ top: rect.top + window.scrollY - dropdownHeight - gap,
261
+ left: rect.left + window.scrollX,
262
+ width: rect.width,
263
+ });
264
+ } else {
265
+ // Dropdown below
266
+ setDropDirection("down");
267
+ setPosition({
268
+ top: rect.bottom + window.scrollY + gap,
269
+ left: rect.left + window.scrollX,
270
+ width: rect.width,
271
+ });
272
+ }
190
273
  };
191
274
 
192
275
  // Focus handlers
@@ -196,7 +279,12 @@ function MultiSelectInputInner<T = any>(
196
279
  const handleFocus = () => {
197
280
  setIsFocused(true);
198
281
  setShowFilters(false);
282
+ setShowSelectedOnly(false);
199
283
  updatePosition();
284
+ // 🟢 First-time fetch trigger
285
+ if (!shouldFetch) {
286
+ setShouldFetch(true);
287
+ }
200
288
  };
201
289
  el.addEventListener("focus", handleFocus);
202
290
  return () => el.removeEventListener("focus", handleFocus);
@@ -212,6 +300,7 @@ function MultiSelectInputInner<T = any>(
212
300
  ) {
213
301
  setIsFocused(false);
214
302
  setShowFilters(false);
303
+ setShowSelectedOnly(false);
215
304
  }
216
305
  };
217
306
  document.addEventListener("mousedown", handleClickOutside);
@@ -278,6 +367,7 @@ function MultiSelectInputInner<T = any>(
278
367
  setInputValue("");
279
368
  setSelectedItems([]);
280
369
  onItemsSelect?.([]);
370
+ if (onClear) onClear();
281
371
  }}
282
372
  className="hover:bg-tertiary/10 hover:text-tertiary size-[38px] p-3 cursor-pointer text-primary/60 rounded transition-colors"
283
373
  />
@@ -287,6 +377,8 @@ function MultiSelectInputInner<T = any>(
287
377
  const dropdown =
288
378
  (isFocused || showFilters) &&
289
379
  Dropdown(
380
+ showSelectedOnly,
381
+ dropDirection,
290
382
  position,
291
383
  isFetching,
292
384
  text,
@@ -295,10 +387,10 @@ function MultiSelectInputInner<T = any>(
295
387
  showFilters,
296
388
  handleFilterChange,
297
389
  items,
390
+ setMaxFetch,
298
391
  renderItem,
299
392
  dropdownRef,
300
393
  maxFetch,
301
- setMaxFetch,
302
394
  STORAGE_KEY,
303
395
  onFiltersChange,
304
396
  handleItemClick,
@@ -310,18 +402,51 @@ function MultiSelectInputInner<T = any>(
310
402
  onItemsSelect
311
403
  );
312
404
 
405
+ const selectedItemsIcon = selectedItems.length > 0 && (
406
+ <div
407
+ onClick={() => {
408
+ setShowFilters(false); // Hide filters panel
409
+ setIsFocused(true); // Open dropdown
410
+ setShowSelectedOnly(true); // Show only selected items
411
+ updatePosition(); // Recalculate dropdown position
412
+ }}
413
+ className="flex items-center hover:bg-tertiary/10 hover:text-tertiary cursor-pointer text-primary/60 rounded transition-colors"
414
+ >
415
+ <List className="size-[38px] p-3" />
416
+ <span className="text-sm px-1">{selectedItems.length}</span>
417
+ </div>
418
+ );
419
+
313
420
  return (
314
- <div ref={wrapperRef}>
315
- <div ref={containerRef} className={cn("w-full", parentClassName)}>
421
+ <div
422
+ ref={wrapperRef}
423
+ className={readOnly ? "pointer-events-none cursor-not-allowed" : ""}
424
+ >
425
+ <div
426
+ ref={containerRef}
427
+ className={cn("w-full relative", rootDivClassName)}
428
+ >
316
429
  <Input
317
430
  ref={ref || inputRef}
318
431
  {...props}
432
+ readOnly={readOnly}
433
+ requiredHint={text.required}
434
+ label={text.label}
319
435
  value={inputValue}
436
+ errorMessage={errorMessage}
320
437
  onChange={inputOnChange}
438
+ startContent={startContent}
321
439
  endContent={
322
440
  <div className="flex items-center gap-1 relative ltr:-right-1 rtl:-left-1">
323
- {isFocused && endIcon}
324
- {filters.length !== 0 && (
441
+ {!showFilters && isFetching ? (
442
+ <LoaderCircle className="size-[38px] p-3 animate-spin" />
443
+ ) : (
444
+ <>
445
+ {selectedItemsIcon}
446
+ {isFocused && endIcon}
447
+ </>
448
+ )}
449
+ {(filters.length !== 0 || showMaxFetch) && (
325
450
  <ListFilter
326
451
  onClick={() => {
327
452
  updatePosition();
@@ -353,6 +478,8 @@ export default MultiSelectInputForward;
353
478
 
354
479
  // ---------------- Dropdown ----------------
355
480
  const Dropdown = <T,>(
481
+ showSelectedOnly: boolean,
482
+ dropDirection: string,
356
483
  position: { top: number; left: number; width: number },
357
484
  isFetching: boolean,
358
485
  text: {
@@ -366,10 +493,10 @@ const Dropdown = <T,>(
366
493
  showFilters: boolean | undefined,
367
494
  handleFilterChange: (key: string, value: boolean) => void,
368
495
  items: T[],
496
+ setMaxFetch: React.Dispatch<React.SetStateAction<number>>,
369
497
  renderItem?: (item: T, selected?: boolean) => React.ReactNode,
370
498
  dropdownRef?: React.Ref<HTMLDivElement>,
371
499
  maxFetch?: number | "",
372
- setMaxFetch?: React.Dispatch<React.SetStateAction<number | "">>,
373
500
  STORAGE_KEY?: string,
374
501
  onFiltersChange?: (filtersState: Record<string, boolean>) => void,
375
502
  handleItemClick?: (item: T) => void,
@@ -380,17 +507,21 @@ const Dropdown = <T,>(
380
507
  setInputValue?: React.Dispatch<React.SetStateAction<string>>,
381
508
  onItemsSelect?: (selected: T | T[]) => void
382
509
  ) =>
510
+ !isFetching &&
383
511
  createPortal(
384
512
  <div
385
513
  ref={dropdownRef}
386
- className="absolute z-50 border border-border rounded-b bg-card shadow-lg pt-3 pb-2"
514
+ className={cn(
515
+ "absolute z-50 border border-border ltr:text-xs ltr:sm:text-sm rtl:text-sm rtl:font-semibold bg-card shadow-lg pb-2",
516
+ dropDirection === "down" ? "rounded-b" : "rounded-t"
517
+ )}
387
518
  style={{ top: position.top, left: position.left, width: position.width }}
388
519
  >
389
520
  {/* Filters Panel */}
390
- {showFilters && filters.length > 0 && (
391
- <div className="pb-3 px-3 flex flex-col gap-2 text-sm">
392
- {filters.map((f) => (
393
- <label key={f.key} className="flex items-center gap-2">
521
+ {
522
+ <div className="pb-3 px-3 flex flex-col gap-2">
523
+ {filters.map((f, index) => (
524
+ <label key={f.key + index} className="flex items-center gap-2">
394
525
  <input
395
526
  type="checkbox"
396
527
  checked={filtersState[f.key]}
@@ -401,24 +532,26 @@ const Dropdown = <T,>(
401
532
  </label>
402
533
  ))}
403
534
 
404
- {setMaxFetch && (
535
+ {(showFilters || filters.length > 0) && (
405
536
  <input
406
537
  type="number"
407
538
  min={1}
408
539
  value={maxFetch}
409
540
  onChange={(e) => {
410
- const value = e.target.value ? Number(e.target.value) : "";
411
- setMaxFetch(value);
412
- if (STORAGE_KEY)
413
- localStorage.setItem(
414
- `${STORAGE_KEY}_MAX_FETCH`,
415
- JSON.stringify(value)
416
- );
541
+ const value = Number(e.target.value);
542
+ if (value) {
543
+ setMaxFetch(value);
544
+ if (STORAGE_KEY)
545
+ localStorage.setItem(
546
+ `${STORAGE_KEY}_MAX_FETCH`,
547
+ JSON.stringify(value)
548
+ );
549
+ }
417
550
  }}
418
551
  className={cn(
419
- "selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded border bg-transparent px-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
552
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 flex w-full min-w-0 rounded-sm border px-3 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-70",
420
553
  "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
421
- "appearance-none placeholder:text-primary/60 ltr:text-sm rtl:text-sm rtl:font-semibold focus-visible:ring-0 focus-visible:shadow-sm focus-visible:ring-offset-0 transition-[border] bg-card dark:bg-black/30",
554
+ "appearance-none placeholder:text-primary/60 ltr:text-sm rtl:text-sm rtl:font-semibold focus-visible:ring-0 focus-visible:shadow-sm focus-visible:ring-offset-0 transition-[border] bg-card",
422
555
  "focus-visible:border-tertiary/60",
423
556
  "[&::-webkit-outer-spin-button]:appearance-none",
424
557
  "[&::-webkit-inner-spin-button]:appearance-none",
@@ -429,7 +562,7 @@ const Dropdown = <T,>(
429
562
  )}
430
563
 
431
564
  {/* Clear Filters + Selected Items */}
432
- {STORAGE_KEY && setMaxFetch && (
565
+ {STORAGE_KEY && showFilters && (
433
566
  <button
434
567
  onClick={() => {
435
568
  // Clear filters
@@ -443,7 +576,7 @@ const Dropdown = <T,>(
443
576
  localStorage.setItem(STORAGE_KEY, JSON.stringify(cleared));
444
577
 
445
578
  // Clear maxFetch
446
- setMaxFetch("");
579
+ setMaxFetch(30);
447
580
  localStorage.removeItem(`${STORAGE_KEY}_MAX_FETCH`);
448
581
 
449
582
  // Clear selected items and input
@@ -460,18 +593,33 @@ const Dropdown = <T,>(
460
593
  </button>
461
594
  )}
462
595
  </div>
463
- )}
464
-
465
- {!showFilters && isFetching && <CircleLoader label={text.fetch} />}
466
-
596
+ }
467
597
  {!showFilters && !isFetching && (
468
598
  <div className="max-h-60 overflow-auto">
469
- {items.length > 0 ? (
599
+ {showSelectedOnly ? (
600
+ selectedItems &&
601
+ selectedItems.length > 0 &&
602
+ selectedItems.map((item, index) => {
603
+ const keyVal = itemKey ? (item as any)[itemKey] : index;
604
+ return renderItem ? (
605
+ renderItem(item, true)
606
+ ) : (
607
+ <div
608
+ key={keyVal}
609
+ className="px-3 flex items-center gap-x-1 py-1 cursor-pointer"
610
+ onClick={() => handleItemClick?.(item)}
611
+ >
612
+ <Check className="size-4" />
613
+ {(item as any)[searchBy ?? "name"]}
614
+ </div>
615
+ );
616
+ })
617
+ ) : items.length > 0 ? (
470
618
  items.map((item, index) => {
471
619
  const keyVal = itemKey ? (item as any)[itemKey] : index;
472
620
  const isSelected =
473
621
  selectedItems?.some(
474
- (i) => itemKey && (i as any)[itemKey] === keyVal
622
+ (i) => itemKey && (i as any)[itemKey] == keyVal
475
623
  ) ?? false;
476
624
 
477
625
  const displayValue = Array.isArray(searchBy)
@@ -484,8 +632,8 @@ const Dropdown = <T,>(
484
632
  <div
485
633
  key={keyVal}
486
634
  className={cn(
487
- "px-3 flex items-center gap-x-1 py-1 hover:bg-gray-100 cursor-pointer",
488
- isSelected && "bg-gray-200"
635
+ "px-3 flex items-center gap-x-1 py-1 hover:bg-primary/5 cursor-pointer",
636
+ isSelected && "bg-primary/5"
489
637
  )}
490
638
  onClick={() => handleItemClick?.(item)}
491
639
  >
@@ -1,5 +1,4 @@
1
- import Input from "../input";
2
- import { InputProps } from "../input/input";
1
+ import Input, { InputProps } from "../input/input";
3
2
  import { Check, X } from "lucide-react";
4
3
  import React, { useMemo, useState } from "react";
5
4
  type PasswordInputText = {
@@ -17,16 +16,36 @@ export interface PasswordInputProps extends InputProps {
17
16
  text: PasswordInputText;
18
17
  }
19
18
  const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
20
- (props, ref: any) => {
21
- const { parentClassName, defaultValue, text, onChange, ...rest } = props;
22
- const [value, setValue] = useState(
23
- typeof defaultValue === "string" ? defaultValue : ""
19
+ (props, ref) => {
20
+ const { classNames, value, text, onChange, ...rest } = props;
21
+ const { rootDivClassName } = classNames || {};
22
+
23
+ // Internal state only if parent does NOT control value
24
+ const [password, setPassword] = useState(value ?? "");
25
+
26
+ // Use parent-controlled value if provided, otherwise internal state
27
+ const currentPassword = value !== undefined ? value : password;
28
+
29
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
30
+ if (value === undefined) {
31
+ setPassword(e.target.value);
32
+ }
33
+ onChange?.(e);
34
+ };
35
+
36
+ const strength = useMemo(
37
+ () =>
38
+ checkStrength(
39
+ typeof currentPassword == "string" ? currentPassword : "",
40
+ text
41
+ ),
42
+ [currentPassword, text]
24
43
  );
25
- const strength = checkStrength(value, text);
26
44
 
27
- const strengthScore = useMemo(() => {
28
- return passwordStrengthScore(strength);
29
- }, [strength]);
45
+ const strengthScore = useMemo(
46
+ () => passwordStrengthScore(strength),
47
+ [strength]
48
+ );
30
49
 
31
50
  const getStrengthColor = (score: number) => {
32
51
  if (score === 0) return "bg-border";
@@ -42,21 +61,18 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
42
61
  if (score === 3) return text.medium_password;
43
62
  return text.strong_password;
44
63
  };
64
+
45
65
  return (
46
- <div className={`w-full ${parentClassName}`}>
66
+ <div className={`w-full ${rootDivClassName ?? ""}`}>
47
67
  <Input
48
- value={value}
68
+ value={currentPassword}
49
69
  ref={ref}
50
- onChange={
51
- onChange
52
- ? onChange
53
- : (event: React.ChangeEvent<HTMLInputElement>) =>
54
- setValue(event.target.value)
55
- }
70
+ onChange={handleChange}
56
71
  aria-invalid={strengthScore < 4}
57
72
  aria-describedby="password-strength"
58
73
  {...rest}
59
74
  />
75
+
60
76
  {/* Password strength indicator */}
61
77
  <div
62
78
  className="mb-4 mt-3 h-1 w-full overflow-hidden rounded-full bg-border"
@@ -71,10 +87,10 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
71
87
  strengthScore
72
88
  )} transition-all duration-500 ease-out`}
73
89
  style={{ width: `${(strengthScore / 4) * 100}%` }}
74
- ></div>
90
+ />
75
91
  </div>
76
92
 
77
- {/* Password strength description */}
93
+ {/* Password strength text */}
78
94
  <p
79
95
  id="password-strength"
80
96
  className="mb-2 text-start rtl:text-xl-rtl ltr:text-xl-ltr font-medium text-foreground"
@@ -82,22 +98,14 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
82
98
  {`${getStrengthText(strengthScore)}. ${text.must_contain}`}
83
99
  </p>
84
100
 
85
- {/* Password requirements list */}
101
+ {/* Requirements */}
86
102
  <ul className="space-y-1.5" aria-label="Password requirements">
87
103
  {strength.map((req, index) => (
88
104
  <li key={index} className="flex items-center gap-2">
89
105
  {req.met ? (
90
- <Check
91
- size={16}
92
- className="text-emerald-500"
93
- aria-hidden="true"
94
- />
106
+ <Check size={16} className="text-emerald-500" />
95
107
  ) : (
96
- <X
97
- size={16}
98
- className="text-muted-foreground/80"
99
- aria-hidden="true"
100
- />
108
+ <X size={16} className="text-muted-foreground/80" />
101
109
  )}
102
110
  <span
103
111
  className={`ltr:text-xs rtl:text-lg-rtl ${
@@ -107,9 +115,6 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
107
115
  }`}
108
116
  >
109
117
  {req.text}
110
- <span className="sr-only rtl:text-xl-rtl">
111
- {req.met ? " - Requirement met" : " - Requirement not met"}
112
- </span>
113
118
  </span>
114
119
  </li>
115
120
  ))}