All files / src/components PasswordInput.tsx

50% Statements 8/16
46.66% Branches 7/15
33.33% Functions 2/6
53.84% Lines 7/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52                292x 292x   292x     35x 35x                   292x   292x                                                
import { useLayoutEffect, useRef, useState } from 'react'
import type { ComponentProps } from 'react'
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
 
export function PasswordInput({
	className = '',
	...props
}: Omit<ComponentProps<'input'>, 'type'>) {
	const [visible, setVisible] = useState(false)
	const inputRef = useRef<HTMLInputElement>(null)
 
	useLayoutEffect(() => {
		// changing the input type re-selects all text in some browsers;
		// collapse the caret to the end so the toggle doesn't leave a selection
		const input = inputRef.current
		Eif (!input || document.activeElement !== input) return
		const end = input.value.length
		input.setSelectionRange(end, end)
		requestAnimationFrame(() => {
			if (inputRef.current === input && document.activeElement === input) {
				input.setSelectionRange(end, end)
			}
		})
	}, [visible])
 
	const toggleVisibility = () => setVisible((v) => !v)
 
	return (
		<div className="relative">
			<input
				ref={inputRef}
				type={visible ? 'text' : 'password'}
				className={`${className} pr-10`}
				{...props}
			/>
			<button
				type="button"
				onMouseDown={(e) => e.preventDefault()}
				onClick={toggleVisibility}
				className="absolute inset-y-0 right-0 flex items-center px-2 text-gray-400 dark:text-neutral-500 cursor-pointer transition-transform duration-100 hover:scale-98"
				aria-label={visible ? 'Hide password' : 'Show password'}
			>
				{visible ? (
					<EyeSlashIcon className="h-5 w-5" />
				) : (
					<EyeIcon className="h-5 w-5" />
				)}
			</button>
		</div>
	)
}