11import * as React from 'react' ;
2+ import { LuX } from 'react-icons/lu' ;
23import { cn } from '@/utils/style' ;
34
45export interface SecretInputProps
@@ -13,6 +14,11 @@ export interface SecretInputProps
1314 * @default "******"
1415 */
1516 maskPlaceholder ?: string ;
17+ /**
18+ * Whether to allow clearing the existing value via the clear button.
19+ * @default true
20+ */
21+ allowClear ?: boolean ;
1622}
1723
1824/**
@@ -42,70 +48,62 @@ const SecretInput = React.forwardRef<HTMLInputElement, SecretInputProps>(
4248 onChange,
4349 maskPlaceholder = '******' ,
4450 placeholder,
51+ allowClear = true ,
52+ disabled,
53+ onFocus,
4554 ...props
4655 } ,
4756 ref
4857 ) => {
4958 const [ internalValue , setInternalValue ] = React . useState ( '' ) ;
50- const [ isFocused , setIsFocused ] = React . useState ( false ) ;
51- const [ hasBeenTouched , setHasBeenTouched ] = React . useState ( false ) ;
59+ const [ hasInteracted , setHasInteracted ] = React . useState ( false ) ;
5260
53- // Check if there's an existing value (from props)
5461 const hasExistingValue = Boolean ( value ) ;
62+ const showMask = hasExistingValue && ! hasInteracted ;
63+ const showClearBtn =
64+ allowClear && ! disabled && ( hasExistingValue || internalValue !== '' ) ;
5565
56- // Determine what placeholder to show
57- const effectivePlaceholder = React . useMemo ( ( ) => {
58- if ( hasBeenTouched ) {
59- // After user has interacted, show normal placeholder
60- return placeholder ;
61- }
62- if ( hasExistingValue && ! isFocused ) {
63- // Show masked placeholder when there's existing value and not focused
64- return maskPlaceholder ;
65- }
66- // Show normal placeholder
67- return placeholder ;
68- } , [
69- hasBeenTouched ,
70- hasExistingValue ,
71- isFocused ,
72- placeholder ,
73- maskPlaceholder ,
74- ] ) ;
75-
76- const handleFocus = ( e : React . FocusEvent < HTMLInputElement > ) => {
77- setIsFocused ( true ) ;
78- setHasBeenTouched ( true ) ;
79- props . onFocus ?.( e ) ;
80- } ;
81-
82- const handleBlur = ( e : React . FocusEvent < HTMLInputElement > ) => {
83- setIsFocused ( false ) ;
84- props . onBlur ?.( e ) ;
85- } ;
86-
87- const handleChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
88- const newValue = e . target . value ;
89- setInternalValue ( newValue ) ;
90- setHasBeenTouched ( true ) ;
91- onChange ?.( newValue ) ;
66+ const emit = ( next : string ) => {
67+ setHasInteracted ( true ) ;
68+ setInternalValue ( next ) ;
69+ onChange ?.( next ) ;
9270 } ;
9371
9472 return (
95- < input
96- type = "password"
97- className = { cn (
98- 'flex h-9 w-full rounded-md border border-zinc-200 bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:placeholder:text-zinc-400 dark:focus-visible:ring-zinc-300' ,
99- className
73+ < div className = "relative w-full" >
74+ < input
75+ { ...props }
76+ ref = { ref }
77+ type = "password"
78+ className = { cn (
79+ 'flex h-9 w-full rounded-md border border-zinc-200 bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:placeholder:text-zinc-400 dark:focus-visible:ring-zinc-300' ,
80+ showClearBtn && 'pr-8' ,
81+ className
82+ ) }
83+ value = { internalValue }
84+ placeholder = { showMask ? maskPlaceholder : placeholder }
85+ disabled = { disabled }
86+ onChange = { ( e ) => emit ( e . target . value ) }
87+ onFocus = { ( e ) => {
88+ setHasInteracted ( true ) ;
89+ onFocus ?.( e ) ;
90+ } }
91+ />
92+ { showClearBtn && (
93+ < button
94+ type = "button"
95+ tabIndex = { - 1 }
96+ aria-label = "Clear"
97+ onClick = { ( e ) => {
98+ e . preventDefault ( ) ;
99+ emit ( '' ) ;
100+ } }
101+ className = "absolute right-2 top-1/2 flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded-sm text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100"
102+ >
103+ < LuX className = "h-3.5 w-3.5" />
104+ </ button >
100105 ) }
101- ref = { ref }
102- value = { internalValue }
103- onChange = { handleChange }
104- onFocus = { handleFocus }
105- onBlur = { handleBlur }
106- placeholder = { effectivePlaceholder }
107- { ...props }
108- />
106+ </ div >
109107 ) ;
110108 }
111109) ;
0 commit comments