// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React, { ChangeEvent, forwardRef, ReactNode, Ref, useEffect, useRef, useState, } from 'react'; import { BaseProps } from '../Base'; import { Clear } from '../icons'; import InputWrapper from './InputWrapper'; import { StyledClear, StyledInput } from './Styled'; export type Size = 'sm' | 'md'; export interface InputProps extends Omit< React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'css' >, BaseProps { /** The callback fired when the state is changed. */ onChange(event: ChangeEvent): void; /** The callback fired when the input value is cleared. */ onClear?(): void; /** The icon in the input. */ leadingIcon?: ReactNode; /** The size of the input. */ sizing?: Size; /** The value of the input. */ value: string; /** The id of the input. */ id?: string; /** Whether or not the clear icon is shown, it defaults to `true`. */ showClear?: boolean; } export const Input = forwardRef( (props: InputProps, externalRef: Ref<HTMLInputElement>) => { const { type, value, sizing, onClear, onChange, className, leadingIcon, showClear = true, ...rest } = props; const [focused, setFocused] = useState(false); const focusedRef = useRef(false); const internalRef = useRef(null); const inputRef = (externalRef || internalRef) as React.MutableRefObject<HTMLInputElement>; const clearRef = useRef<HTMLButtonElement>(null); const label = props['aria-label'] ? `clear ${props['aria-label']}` : 'clear'; const handleClear = () => { if (onClear) { onClear(); return; } const input = inputRef.current; const nativeSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value' )?.set; if (nativeSetter && input) { nativeSetter.call(input, ''); input.dispatchEvent(new Event('input', { bubbles: true })); } input.focus(); }; useEffect(() => { let blurring = false; const onFocus = (e: any) => { if (!focusedRef.current) { return; } if (e.target !== clearRef.current && e.target !== inputRef.current) { focusedRef.current = false; setFocused(false); return; } if (blurring) { blurring = false; } }; const onFocusOut = (): void => { if (!focusedRef.current) { return; } blurring = true; setTimeout(() => { if (blurring) { focusedRef.current = false; setFocused(false); } blurring = false; }, 10); }; document.addEventListener('focusin', onFocus); document.addEventListener('focusout', onFocusOut); return () => { document.removeEventListener('focusin', onFocus); document.removeEventListener('focusout', onFocusOut); }; }, []); return ( <InputWrapper leadingIcon={leadingIcon} sizing={sizing} className={`ch-input-wrapper ${className || ''}`} > <StyledInput {...rest} value={value} type={type || 'text'} ref={inputRef} className="ch-input" onChange={onChange} data-testid="input" onFocus={() => { focusedRef.current = true; setFocused(true); }} /> {showClear && ( <StyledClear type="button" active={!!(onClear || (focused && value.length))} tabIndex={-1} aria-label={label} onClick={handleClear} ref={clearRef} > <Clear width="1.25rem" /> </StyledClear> )} </InputWrapper> ); } ); Input.displayName = 'Input'; export default Input;