import { ComponentProps, forwardRef, Fragment, ReactNode } from 'react'; import classnames from 'classnames'; import AccessibilityLinkActionIcon from '../accessibility/AccessibilityLinkActionIcon'; import { useRequestAnimationFrame } from '../animation/RequestAnimationFrameContext'; import Avatar from '../Avatar'; import Badge from '../Badge'; import Box from '../Box'; import { useDeviceType } from '../contexts/DeviceTypeProvider'; import styles from '../Dropdown.css'; import Flex from '../Flex'; import focusStyles from '../Focus.css'; import getRoundingClassName from '../getRoundingClassName'; import Icon from '../Icon'; import tapAreaStyles from '../TapArea.css'; import TapAreaLink from '../TapAreaLink'; import Text from '../Text'; import { FontWeight } from '../textTypes'; import TextUI from '../TextUI'; import useFocusVisible from '../useFocusVisible'; import useExperimentalTheme from '../utils/useExperimentalTheme'; export type OptionItemType = { label: string; subtext?: string; value: string; }; type BadgeType = { text: string; type?: | 'info' | 'error' | 'warning' | 'success' | 'neutral' | 'recommendation' | 'darkWash' | 'lightWash'; }; type IconEndType = 'visit' | 'directional-arrow-right' | 'download'; type Props = { avatar?: Omit, 'size' | 'verified' | 'outline'> & { size: 'sm' | 'md'; }; badge?: BadgeType; children?: ReactNode; dataTestId?: string; disabled?: boolean; focusedItemIndex: number | null | undefined; hoveredItemIndex: number | null | undefined; href?: string; id: string; index: number; iconEnd?: IconEndType; onClick?: (arg1: { event: React.MouseEvent | React.KeyboardEvent; dangerouslyDisableOnNavigation: () => void; mobileOnDismissStart: () => void; }) => void; onSelect?: (arg1: { item: OptionItemType; event: React.ChangeEvent }) => void; option: OptionItemType; selected?: OptionItemType | ReadonlyArray | null; setHoveredItemIndex: (arg1: number | null | undefined) => void; setFocusedItemIndex: (arg1: number | null | undefined) => void; textWeight?: FontWeight; }; const OptionItemWithForwardRef = forwardRef( function OptionItem( { avatar, badge, children, dataTestId, disabled, onSelect, focusedItemIndex, hoveredItemIndex, setFocusedItemIndex, href, id, index, iconEnd, onClick, option, selected, setHoveredItemIndex, textWeight = 'normal', }: Props, ref, ) { const deviceType = useDeviceType(); const isMobile = deviceType === 'mobile'; const { onExternalDismiss } = useRequestAnimationFrame(); const matches = (Array.isArray(selected) ? selected : []).filter( ({ value }) => value === option.value, ); // Determine if the option is a current selected item const isSelectedItem = matches.length > 0 || JSON.stringify(option) === JSON.stringify(selected); const { isFocusVisible } = useFocusVisible(); const theme = useExperimentalTheme(); const className = classnames(getRoundingClassName(2), focusStyles.hideOutlineWithin, { [styles.hovered]: theme.MAIN && index === hoveredItemIndex, [focusStyles.hideOutline]: index !== focusedItemIndex || index === hoveredItemIndex, [focusStyles.accessibilityOutlineFocus]: !theme.MAIN && index === focusedItemIndex && isFocusVisible, [focusStyles.accessibilityVROutlineFocus]: theme.MAIN && index === focusedItemIndex && isFocusVisible, [tapAreaStyles.fullWidth]: true, [tapAreaStyles.pointer]: !disabled, [tapAreaStyles.noDrop]: disabled, }); const textColor = disabled ? 'disabled' : 'default'; const optionItemContent = ( {avatar ? ( {/* Adds a pause for screen reader users between the text content */} {`, `} ) : null} {children || ( {theme.MAIN ? ( {option?.label} ) : ( {option?.label} )} {badge && !disabled && ( {/* Adds a pause for screen reader users between the text content */} {`, `} )} )} {option.subtext && theme.MAIN ? ( {option.subtext} ) : null} {option.subtext && !theme.MAIN ? ( {option.subtext} ) : null} {isSelectedItem && !iconEnd ? ( ) : ( )} {iconEnd && ( )} ); return (
' is not assignable to type 'LegacyRef | undefined'. ref={index === hoveredItemIndex || index === focusedItemIndex ? ref : null} aria-disabled={disabled} className={className} data-test-id={dataTestId} // These event.stopPropagation are important so interactive anchors don't receive the onFocus/onBlur event id={`${id}-item-${index}`} onBlur={(event) => event.stopPropagation()} // @ts-expect-error - TS2322 - Type '(event: React.ChangeEvent) => void' is not assignable to type 'MouseEventHandler'. onClick={(event: React.ChangeEvent) => { if (!href && !children) event.preventDefault(); if (disabled) return; onSelect?.({ event, item: option }); }} // This event.stopPropagation is important so interactive anchors don't compress with the onMouseDown event onFocus={(event) => event.stopPropagation()} onKeyPress={(event) => { event.preventDefault(); }} onMouseDown={(event) => { event.stopPropagation(); event.preventDefault(); }} onMouseEnter={() => { if (!disabled) { setFocusedItemIndex(null); setHoveredItemIndex(index); } }} role="menuitem" rounding={2} tabIndex={isMobile && !disabled ? 0 : -1} > {href && !disabled ? ( onClick?.({ event, dangerouslyDisableOnNavigation, mobileOnDismissStart: isMobile ? onExternalDismiss : () => {}, }) } target={iconEnd === 'visit' ? 'blank' : 'self'} > {optionItemContent} ) : ( optionItemContent )}
); }, ); OptionItemWithForwardRef.displayName = 'OptionItem'; export default OptionItemWithForwardRef;