import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import classnames from 'classnames'; import Box from './Box'; import { useDefaultLabelContext } from './contexts/DefaultLabelProvider'; import Icon from './Icon'; import IconButton from './IconButton'; import layout from './Layout.css'; import styles from './SearchField.css'; import VRSearchField from './SearchField/VRSearchField'; import formElement from './sharedSubcomponents/FormElement.css'; import FormErrorMessage from './sharedSubcomponents/FormErrorMessage'; import FormLabel from './sharedSubcomponents/FormLabel'; import useExperimentalTheme from './utils/useExperimentalTheme'; type UnionRefs = HTMLDivElement | HTMLAnchorElement; type Props = { /** * String that clients such as VoiceOver will read to describe the element. Always localize the label. See the [Accessibility section](https://gestalt.pinterest.systems/web/searchfield#Accessibility) for more info. */ accessibilityLabel: string; /** * String that clients such as VoiceOver will read to describe the clear button element. Always localize the label. See the [Accessibility section](https://gestalt.pinterest.systems/web/searchfield#Accessibility) for more info. */ accessibilityClearButtonLabel?: string; /** * The type of autocomplete used, if any. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) for more info. */ autoComplete?: 'on' | 'off' | 'username' | 'name'; /** * Available for testing purposes, if needed. Consider [better queries](https://testing-library.com/docs/queries/about/#priority) before using this prop. */ dataTestId?: string; /** * Error text displayed below the input field. */ errorMessage?: string; /** * Must be unique! */ id: string; /** * Text used to label the input. Be sure to localize the text. See the [Visible label variant](https://gestalt.pinterest.systems/web/searchfield#Visible-label) for more info. */ label?: string; /** * Whether the legend should be visible or not. If `hidden`, the legend is still available for screen reader users, but does not appear visually. See the [label visibility variant](https://gestalt.pinterest.systems/web/searchfield#Visible-label) for more info. */ labelDisplay?: 'visible' | 'hidden'; /** * */ onBlur?: (arg1: { event: React.FocusEvent; value: string }) => void; /** * Primary callback to handle keyboard input. */ onChange: (arg1: { value: string; event: React.SyntheticEvent; }) => void; /** * Callback when user clicks on clear button. */ onClear?: () => void; /** * */ onFocus?: (arg1: { value: string; event: React.FocusEvent }) => void; /** * Secondary callback for keyboard events. Possible uses include validation, form submission, etc. */ onKeyDown?: (arg1: { event: React.KeyboardEvent; value: string }) => void; /** * Text displayed before the user has entered anything. */ placeholder?: string; /** * Indicate if the input is readOnly. */ readOnly?: boolean; /** * Ref that is forwarded to the underlying input element. */ ref?: UnionRefs; // eslint-disable-line react/no-unused-prop-types, size?: 'md' | 'lg'; /** * Use "-1" to remove SearchField from keyboard navigation. */ tabIndex?: -1 | 0; /** * The current value of the input. */ value?: string; }; /** * [SearchField](https://gestalt.pinterest.systems/web/searchfield) allows users to search for free-form content. * * ![SearchField light mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/SearchField.spec.ts-snapshots/SearchField-chromium-darwin.png) * ![SearchField dark mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/SearchField-dark.spec.ts-snapshots/SearchField-dark-chromium-darwin.png) * */ const SearchFieldWithForwardRef = forwardRef(function SearchField( { accessibilityLabel, accessibilityClearButtonLabel, autoComplete, dataTestId, id, label, labelDisplay = 'visible', onBlur, onChange, onClear, onFocus, onKeyDown, placeholder, size = 'md', value, errorMessage, readOnly, tabIndex, }: Props, ref, ) { const theme = useExperimentalTheme(); const { accessibilityClearButtonLabel: accessibilityDefaultClearButtonLabel } = useDefaultLabelContext('SearchField'); const [hovered, setHovered] = useState(false); const [focused, setFocused] = useState(false); // Ref to the input const inputRef = useRef(null); // @ts-expect-error - TS2322 - Type 'HTMLInputElement | null' is not assignable to type 'HTMLInputElement'. useImperativeHandle(ref, () => inputRef.current); const handleChange = (event: React.SyntheticEvent) => { onChange({ value: event.currentTarget.value, event, }); }; const handleMouseEnter = () => setHovered(true); const handleMouseLeave = () => setHovered(false); const handleFocus = (event: React.FocusEvent) => { setFocused(true); if (onFocus) { onFocus({ value: event.currentTarget.value, event, }); } }; const handleBlur = (event: React.FocusEvent) => { setFocused(false); if (onBlur) { onBlur({ value: event.currentTarget.value, event }); } }; const handleKeyDown = (event: React.KeyboardEvent) => { if (onKeyDown) { onKeyDown({ event, value: event.currentTarget.value }); } }; const hasValue = value && value.length > 0; const hideSearchIcon = focused || hasValue; const className = classnames( styles.input, { [layout.medium]: size === 'md', [layout.large]: size === 'lg', [styles.inputActive]: focused || hasValue, [styles.inputHovered]: hovered, }, errorMessage ? formElement.errored : formElement.normal, ); if (theme.MAIN) { return ( ); } return ( {label && } {!hideSearchIcon && ( )} {/* @ts-expect-error - TS2322 - Type '{ ref: MutableRefObject; "aria-describedby": string | null; "aria-invalid": "true" | "false"; "aria-label": string; autoComplete: "name" | ... 3 more ... | undefined; ... 7 more ...; value: string | undefined; }' is not assignable to type 'DetailedHTMLProps, HTMLInputElement>'. */} {hasValue && !readOnly && (
{ inputRef?.current?.focus(); onChange({ value: '', event }); onClear?.(); }} padding={size === 'md' ? 1 : undefined} selected={focused} size="xs" />
)}
{errorMessage && }
); }); SearchFieldWithForwardRef.displayName = 'SearchField'; export default SearchFieldWithForwardRef;