import { forwardRef, ReactNode, useImperativeHandle, useRef } from 'react'; import classnames from 'classnames'; import { AriaCurrent } from '../ariaTypes'; import buttonStyles from '../Button.css'; import { useGlobalEventsHandlerContext } from '../contexts/GlobalEventsHandlerProvider'; import focusStyles from '../Focus.css'; import getRoundingClassName, { Rounding } from '../getRoundingClassName'; import iconButtonStyles from '../IconButton/InternalIconButton.css'; import layoutStyles from '../Layout.css'; import searchGuideStyles from '../SearchGuide.css'; import touchableStyles from '../TapArea.css'; import styles from '../Text.css'; import useFocusVisible from '../useFocusVisible'; import useTapFeedback, { keyPressShouldTriggerTap } from '../useTapFeedback'; import useExperimentalTheme from '../utils/useExperimentalTheme'; type Props = { accessibilityCurrent?: AriaCurrent; accessibilityLabel?: string; children?: ReactNode; colorClass?: string | Array; dataTestId?: string; disabled?: boolean; download?: boolean | string; focusColor?: 'lightBackground' | 'darkBackground'; fullHeight?: boolean; fullWidth?: boolean; href: string; id?: string; innerFocusColor?: 'default' | 'inverse'; mouseCursor?: 'copy' | 'grab' | 'grabbing' | 'move' | 'noDrop' | 'pointer' | 'zoomIn' | 'zoomOut'; onClick?: (arg1: { event: React.MouseEvent | React.KeyboardEvent; dangerouslyDisableOnNavigation: () => void; }) => void; onBlur?: (arg1: { event: React.FocusEvent }) => void; onFocus?: (arg1: { event: React.FocusEvent }) => void; onKeyDown?: (arg1: { event: React.KeyboardEvent }) => void; onMouseDown?: (arg1: { event: React.MouseEvent | React.MouseEvent; }) => void; onMouseUp?: (arg1: { event: React.MouseEvent | React.MouseEvent; }) => void; onMouseEnter?: (arg1: { event: React.MouseEvent | React.MouseEvent; }) => void; onMouseLeave?: (arg1: { event: React.MouseEvent | React.MouseEvent; }) => void; rel?: 'none' | 'nofollow'; tabIndex: -1 | 0; rounding?: Rounding; selected?: boolean; size?: 'sm' | 'md' | 'lg'; tapStyle?: 'none' | 'compress'; target?: null | 'self' | 'blank'; title?: string; wrappedComponent: 'button' | 'iconButton' | 'tapArea' | 'searchGuide'; }; const InternalLinkWithForwardRef = forwardRef(function Link( { accessibilityCurrent, accessibilityLabel, children, colorClass, dataTestId, disabled, download, focusColor = 'lightBackground', fullHeight, fullWidth, href, id, innerFocusColor, mouseCursor, onClick, onBlur, onFocus, onKeyDown, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, rel, tabIndex = 0, rounding, selected, size, tapStyle = 'compress', target, title, wrappedComponent, }: Props, ref, ) { const innerRef = useRef(null); // @ts-expect-error - TS2322 - Type 'HTMLAnchorElement | null' is not assignable to type 'HTMLAnchorElement'. useImperativeHandle(ref, () => innerRef.current); const { compressStyle, handleBlur, handleMouseDown, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchCancel, handleTouchEnd, isTapping, } = useTapFeedback({ height: innerRef?.current?.clientHeight, width: innerRef?.current?.clientWidth, }); const theme = useExperimentalTheme(); const { isFocusVisible } = useFocusVisible(); const isTapArea = wrappedComponent === 'tapArea'; const isButton = wrappedComponent === 'button'; const isIconButton = wrappedComponent === 'iconButton'; const isSearchGuide = wrappedComponent === 'searchGuide'; const className = classnames( styles.noOutline, styles.inheritColor, styles.noUnderline, touchableStyles.tapTransition, isTapArea ? getRoundingClassName(rounding || 0) : undefined, !isTapArea && !theme.MAIN ? getRoundingClassName('pill') : undefined, { [touchableStyles.tapCompress]: !disabled && tapStyle === 'compress' && isTapping, [focusStyles.hideOutline]: !disabled && !isFocusVisible, [focusStyles.accessibilityOutline]: !disabled && isFocusVisible && !theme.MAIN, }, isButton && !theme.MAIN ? { [layoutStyles.inlineFlex]: !fullWidth, [layoutStyles.flex]: fullWidth, [layoutStyles.justifyCenter]: true, [layoutStyles.xsItemsCenter]: true, [buttonStyles.button]: true, [buttonStyles.disabled]: disabled, [buttonStyles.selected]: !disabled && selected, [buttonStyles.sm]: size === 'sm', [buttonStyles.md]: size === 'md', [buttonStyles.lg]: size === 'lg', } : {}, isButton && theme.MAIN ? { [layoutStyles.inlineFlex]: !fullWidth, [layoutStyles.flex]: fullWidth, [layoutStyles.justifyCenter]: true, [layoutStyles.xsItemsCenter]: true, [buttonStyles.buttonVr]: true, [buttonStyles.disabled]: disabled, [buttonStyles.selected]: !disabled && selected, [buttonStyles.smLinkVr]: size === 'sm', [buttonStyles.mdLinkVr]: size === 'md', [buttonStyles.lgLinkVr]: size === 'lg', [buttonStyles.vrFocused]: !disabled && isFocusVisible, [buttonStyles.defaultFocus]: !disabled && isFocusVisible && focusColor === 'lightBackground', [buttonStyles.inverseFocus]: !disabled && isFocusVisible && focusColor === 'darkBackground', } : {}, isButton && colorClass ? { [buttonStyles[colorClass as keyof typeof buttonStyles]]: !disabled && !selected, } : {}, isTapArea ? { [layoutStyles.block]: true, [touchableStyles.fullHeight]: fullHeight, [touchableStyles.fullWidth]: fullWidth, [focusStyles.accessibilityOutlineLightBackground]: theme.MAIN && focusColor === 'lightBackground' && !disabled && isFocusVisible, [focusStyles.accessibilityOutlineDarkBackground]: theme.MAIN && focusColor === 'darkBackground' && !disabled && isFocusVisible, [focusStyles.accessibilityOutlineBorder]: theme.MAIN && innerFocusColor === 'default' && !disabled && !isFocusVisible, [focusStyles.accessibilityOutlineBorderDefault]: theme.MAIN && innerFocusColor === 'default' && !disabled && isFocusVisible, [focusStyles.accessibilityOutlineBorderInverse]: theme.MAIN && innerFocusColor === 'inverse' && !disabled && isFocusVisible, } : {}, isTapArea && mouseCursor ? { [touchableStyles[mouseCursor]]: !disabled, } : {}, isIconButton ? { [iconButtonStyles.button]: true, [iconButtonStyles.disabled]: disabled, [iconButtonStyles.enabled]: !disabled, } : {}, ); const searchGuideClassNames = classnames( styles.noUnderline, touchableStyles.tapTransition, isSearchGuide && theme.MAIN ? { [searchGuideStyles.searchguideVr]: true, [focusStyles.hideOutline]: !isFocusVisible, [searchGuideStyles.vrFocused]: isFocusVisible, } : {}, isSearchGuide && !theme.MAIN ? { [searchGuideStyles[colorClass as keyof typeof searchGuideStyles]]: typeof colorClass === 'string', [searchGuideStyles.searchguide]: true, [focusStyles.hideOutline]: !isFocusVisible, [focusStyles.accessibilityOutline]: isFocusVisible, } : {}, ); // Consumes GlobalEventsHandlerProvider const { linkHandlers } = useGlobalEventsHandlerContext() ?? { linkHandlers: { onNavigation: undefined }, }; const { onNavigation } = linkHandlers ?? { onNavigation: undefined }; const onNavigationHandler = onNavigation?.({ href, target }); const handleKeyPress = (event: React.KeyboardEvent) => { // Check to see if space or enter were pressed if (onClick && keyPressShouldTriggerTap(event)) { // Prevent the default action to stop scrolling when space is pressed event.preventDefault(); onClick({ event, dangerouslyDisableOnNavigation: () => {} }); } }; const inBackgroundGradient = !theme.MAIN && typeof colorClass !== 'string' && Array.isArray(colorClass); const isCompressed = tapStyle === 'compress' && compressStyle && !disabled; return ( { onBlur?.({ event }); handleBlur(); }} onClick={(event) => { let defaultOnNavigationIsEnabled = true; const dangerouslyDisableOnNavigation = () => { defaultOnNavigationIsEnabled = false; }; onClick?.({ event, dangerouslyDisableOnNavigation, }); if (onNavigationHandler && defaultOnNavigationIsEnabled) { onNavigationHandler({ event }); } }} onFocus={(event) => { onFocus?.({ event }); }} onKeyDown={(event) => { onKeyDown?.({ event }); }} onKeyPress={handleKeyPress} onMouseDown={(event) => { onMouseDown?.({ event }); handleMouseDown(); }} onMouseEnter={(event) => { onMouseEnter?.({ event }); }} onMouseLeave={(event) => { onMouseLeave?.({ event }); }} onMouseUp={(event) => { onMouseUp?.({ event }); handleMouseUp(); }} onTouchCancel={handleTouchCancel} onTouchEnd={handleTouchEnd} onTouchMove={handleTouchMove} onTouchStart={handleTouchStart} rel={[ ...(target === 'blank' ? ['noopener', 'noreferrer'] : []), ...(rel === 'nofollow' ? ['nofollow'] : []), ].join(' ')} {...(inBackgroundGradient || isCompressed ? { style: { ...(inBackgroundGradient ? { backgroundImage: `linear-gradient(0.25turn, ${colorClass.join(', ')})`, } : {}), ...(isCompressed ? compressStyle : {}), }, } : {})} tabIndex={disabled ? undefined : tabIndex} target={target ? `_${target}` : undefined} title={title} {...(download ? { download } : {})} > {children} ); }); InternalLinkWithForwardRef.displayName = 'InternalLink'; export default InternalLinkWithForwardRef;