import * as React from "react"; import { EyeIcon, EyeSlashIcon, CopyIcon, CheckIcon } from "@phosphor-icons/react"; import { cn } from "../../utils.js"; import { copyToClipboard } from "./clipboard-text.js"; import "./sensitive-input.css"; export interface SensitiveInputProps extends Omit, "type"> { revealed?: boolean; defaultRevealed?: boolean; onRevealedChange?: (revealed: boolean) => void; showCopy?: boolean; containerClassName?: string; } interface SensitiveInputContextValue { revealed: boolean; toggleRevealed: () => void; disabled?: boolean; readOnly?: boolean; value?: string | number | readonly string[]; } const SensitiveInputContext = React.createContext(null); export function useSensitiveInputContext() { const context = React.useContext(SensitiveInputContext); if (!context) { throw new Error( "SensitiveInput compound subcomponents must be used within SensitiveInputContainer" ); } return context; } export const SensitiveInputContainer = React.forwardRef< HTMLDivElement, React.HTMLAttributes & { revealed?: boolean; defaultRevealed?: boolean; onRevealedChange?: (revealed: boolean) => void; disabled?: boolean; readOnly?: boolean; value?: string | number | readonly string[]; } >( ( { className, revealed: controlledRevealed, defaultRevealed = false, onRevealedChange, disabled, readOnly, value, children, ...props }, ref ) => { const [uncontrolledRevealed, setUncontrolledRevealed] = React.useState(defaultRevealed); const isRevealed = controlledRevealed !== undefined ? controlledRevealed : uncontrolledRevealed; const toggleRevealed = React.useCallback(() => { const next = !isRevealed; if (controlledRevealed === undefined) { setUncontrolledRevealed(next); } onRevealedChange?.(next); }, [isRevealed, controlledRevealed, onRevealedChange]); return (
{children}
); } ); SensitiveInputContainer.displayName = "SensitiveInputContainer"; export const SensitiveInputRoot = SensitiveInputContainer; export interface SensitiveInputFieldProps extends React.InputHTMLAttributes {} export const SensitiveInputField = React.forwardRef< HTMLInputElement, SensitiveInputFieldProps >(({ className, ...props }, ref) => { const context = React.useContext(SensitiveInputContext); const isRevealed = context ? context.revealed : false; return ( ); }); SensitiveInputField.displayName = "SensitiveInputField"; export interface SensitiveInputRevealProps extends React.ButtonHTMLAttributes {} export const SensitiveInputReveal = React.forwardRef< HTMLButtonElement, SensitiveInputRevealProps >(({ className, type = "button", onClick, children, ...props }, ref) => { const { revealed, toggleRevealed, disabled } = useSensitiveInputContext(); const handleClick = (e: React.MouseEvent) => { onClick?.(e); if (!e.defaultPrevented) { toggleRevealed(); } }; return ( ); }); SensitiveInputReveal.displayName = "SensitiveInputReveal"; export const SensitiveInputToggle = SensitiveInputReveal; export interface SensitiveInputCopyProps extends React.ButtonHTMLAttributes { textToCopy?: string; copyTimeout?: number; } export const SensitiveInputCopy = React.forwardRef< HTMLButtonElement, SensitiveInputCopyProps >( ( { className, type = "button", textToCopy, copyTimeout = 2000, onClick, children, ...props }, ref ) => { const { value, disabled } = useSensitiveInputContext(); const [copied, setCopied] = React.useState(false); const handleCopy = async (e: React.MouseEvent) => { onClick?.(e); if (e.defaultPrevented || disabled) return; const targetText = textToCopy ?? (typeof value === "string" ? value : ""); if (!targetText) return; const success = await copyToClipboard(targetText); if (success) { setCopied(true); setTimeout(() => setCopied(false), copyTimeout); } }; return ( ); } ); SensitiveInputCopy.displayName = "SensitiveInputCopy"; /* Convenience component forwarding ref to the input element */ export const SensitiveInput = React.forwardRef< HTMLInputElement, SensitiveInputProps >( ( { className, containerClassName, revealed, defaultRevealed = false, onRevealedChange, showCopy = false, disabled, readOnly, value, ...props }, ref ) => { return ( {showCopy && ( )} ); } ); SensitiveInput.displayName = "SensitiveInput";