import React, { cloneElement, forwardRef, ReactElement, useImperativeHandle, useRef } from 'react'; import classnames from 'classnames'; import Box from './Box'; import Flex from './Flex'; import focusStyles from './Focus.css'; import IconCompact from './IconCompact'; import styles from './SearchGuide.css'; import touchableStyles from './TapArea.css'; import TextUI from './TextUI'; import useFocusVisible from './useFocusVisible'; import useTapFeedback from './useTapFeedback'; import useExperimentalTheme from './utils/useExperimentalTheme'; type Props = { /** * Specifies the id of an associated element (or elements) whose contents or visibility are controlled by SearchGuide so that screen reader users can identify the relationship between elements. */ accessibilityControls?: string; /** * Indicates that SearchGuide hides or exposes collapsible components and exposes whether they are currently expanded or collapsed. */ accessibilityExpanded?: boolean; /** * Indicates that a component controls the appearance of interactive popup elements, such as menu or dialog. See the [Accessibility guidelines](https://gestalt.pinterest.systems/web/searchguide#ARIA-attributes) for details on proper usage. */ accessibilityHaspopup?: boolean; /** * Label for screen readers to announce SearchGuide. See the [Accessibility guidelines](https://gestalt.pinterest.systems/web/searchguide#ARIA-attributes) for details on proper usage. */ accessibilityLabel?: string; /** * The background color of SearchGuide. * See the [color variant](https://gestalt.pinterest.systems/web/searchguide#Colors) for implementation guidance. */ color?: | '01' | '02' | '03' | '04' | '05' | '06' | '07' | '08' | '09' | '10' | '11' | ReadonlyArray; /** * Available for testing purposes, if needed. Consider [better queries](https://testing-library.com/docs/queries/about/#priority) before using this prop. */ dataTestId?: string; /** * Indicates that the SearchGuide is expandable. See the [expandable variant](https://gestalt.pinterest.systems/web/searchguide#Expandable) to learn more. */ expandable?: boolean; /** * Callback invoked when the user clicks (press and release) on SearchGuide with the mouse or keyboard. */ onClick?: (arg1: { event: React.MouseEvent | React.KeyboardEvent; }) => void; /** * Toggles between binary states: on/off, selected/unselected, open/closed. See the [selected](#Selected-state) variant to learn more. See the [state variant](https://gestalt.pinterest.systems/web/searchguide#State) for details on proper usage. */ selected?: boolean; /** * Text to render inside the SearchGuide to convey the function and purpose of the SearchGuide. * * It can be empty, but is still required as a fallback to accessibilityLabel. */ text: string; /** * The thumbnail prop is used to display an image or icon to the left of the text. See the [thumbnail variant](https://gestalt.pinterest.systems/web/searchguide#Thumbnail) to learn more. */ thumbnail?: | { avatar: ReactElement } | { avatarGroup: ReactElement } | { image: ReactElement } | { icon: ReactElement }; }; /** * [SearchGuide](https://gestalt.pinterest.systems/web/searchguide) appends and refines a search query. They appear under [SearchField](/web/searchfield) after user submits a search input. * * ![SearchGuide light mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/SearchGuide.spec.ts-snapshots/SearchGuide-chromium-darwin.png) * ![SearchGuide dark mode](https://raw.githubusercontent.com/pinterest/gestalt/master/playwright/visual-test/SearchGuide-dark.spec.ts-snapshots/SearchGuide-dark-chromium-darwin.png) * */ const SearchGuideWithForwardRef = forwardRef(function SearchGuide( { accessibilityControls, accessibilityExpanded, accessibilityHaspopup, accessibilityLabel, color = '01', dataTestId, expandable = false, onClick, selected = false, text, thumbnail, }: Props, ref, ) { const theme = useExperimentalTheme(); const innerRef = useRef(null); // When using both forwardRef and innerRef, React.useimperativehandle() allows a parent component // that renders to call inputRef.current.focus() // @ts-expect-error - TS2322 - Type 'HTMLButtonElement | null' is not assignable to type 'HTMLButtonElement'. useImperativeHandle(ref, () => innerRef.current); const { isFocusVisible } = useFocusVisible(); const colorClass: { [key: string]: keyof typeof styles } = { '01': 'color01', '02': 'color02', '03': 'color03', '04': 'color04', '05': 'color05', '06': 'color06', '07': 'color07', '08': 'color08', '09': 'color09', '10': 'color10', '11': 'color11', }; const colorClassname = typeof color === 'string' ? styles[colorClass[color]!] : null; const style = theme.MAIN ? classnames(styles.searchguideVr, touchableStyles.tapTransition, { [focusStyles.hideOutline]: !isFocusVisible, [styles.vrFocused]: isFocusVisible, [styles.selectedVr]: selected, [styles.gradient]: typeof color !== 'string' && Array.isArray(color) && !isFocusVisible && !selected, }) : classnames(styles.searchguide, touchableStyles.tapTransition, !selected && colorClassname, { [styles.selected]: selected, [focusStyles.hideOutline]: !isFocusVisible && !selected, [focusStyles.accessibilityOutline]: isFocusVisible, }); const textComponent = text.length > 0 ? ( {text} ) : null; const expandableIcon = ( ); const thumbnailVariant = thumbnail && ( {'avatar' in thumbnail && ( {cloneElement(thumbnail.avatar, { size: 'fit', outline: true })} )} {'avatarGroup' in thumbnail && ( {cloneElement(thumbnail.avatarGroup, { size: 'sm' })} )} {'image' in thumbnail && (
{cloneElement(thumbnail.image, { fit: 'cover' })}
)} {'icon' in thumbnail && ( {cloneElement(thumbnail.icon, { color: selected ? 'inverse' : 'dark', })} )} {text.length > 0 && textComponent} {expandable ? expandableIcon : null}
); const textVariant = ( {textComponent} {expandable ? expandableIcon : null} ); const { compressStyle, handleBlur, handleMouseDown, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchCancel, handleTouchEnd, isTapping, } = useTapFeedback({ height: innerRef?.current?.clientHeight, width: innerRef?.current?.clientWidth, }); const variant = thumbnail ? thumbnailVariant : textVariant; const inBackgroundGradient = !theme.MAIN && typeof color !== 'string' && Array.isArray(color); return ( ); }); SearchGuideWithForwardRef.displayName = 'SearchGuide'; export default SearchGuideWithForwardRef;