All files / src/components chip.tsx

100% Statements 15/15
100% Branches 16/16
100% Functions 2/2
100% Lines 15/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105                                                                                                  9x                       9x 9x   9x   1x 1x       9x 9x 9x 3x 3x 1x 1x       9x 9x                                            
import {
  type FC,
  useCallback,
  useRef,
  type ReactNode,
  type MouseEvent as ReactMouseEvent,
  type HTMLAttributes,
} from 'react';
import cn from 'classnames';
 
import RemoveIcon from '../svg/times.svg';
 
import '../styles/components/chip.scss';
 
type ChipProps = {
  /**
   * What is displayed within the chip
   */
  children: string | ReactNode;
  /**
   * Call back which, if present, will display a remove icon and is fired when this is clicked
   */
  onRemove?: (event: ReactMouseEvent<SVGElement, MouseEvent>) => void;
  /**
   * If true will opacify the chip and prevent the remove from being clickable
   */
  disabled?: boolean;
  /**
   * Additional CSS classnames to apply (eg secondary, tertiary)
   */
  className?: string;
  /**
   * Compact styling for chip
   */
  compact?: boolean;
  /**
   * click event listener on the component (except on the close button if present)
   */
  onClick?: () => void;
  /**
   * key press event listener on the component
   */
  onKeyPress?: () => void;
  innerRef?: (node: HTMLElement | null) => void;
  asSpan?: boolean;
};
 
export const Chip: FC<
  ChipProps & HTMLAttributes<HTMLButtonElement | HTMLSpanElement>
> = ({
  children,
  onRemove,
  className = '',
  disabled,
  compact = false,
  onClick,
  onKeyPress,
  innerRef,
  asSpan = false,
  ...rest
}) => {
  const onRemoveRef = useRef(onRemove);
  onRemoveRef.current = onRemove;
 
  const handleRemove = useCallback(
    (event: ReactMouseEvent<SVGElement, MouseEvent>) => {
      event.stopPropagation();
      onRemoveRef.current?.(event);
    },
    []
  );
  const props = { ...rest };
  let element: 'button' | 'span' = 'button';
  if (onRemove || asSpan) {
    element = 'span';
    if (onClick || onKeyPress) {
      props.role = 'button';
      props.tabIndex = 0;
    }
  }
 
  const Element = element;
  return (
    <Element
      ref={innerRef}
      className={cn(
        'chip',
        { 'chip--disabled': disabled, 'chip--compact': compact },
        className
      )}
      type={element === 'button' ? 'button' : undefined}
      onKeyPress={onKeyPress}
      onClick={onClick}
      {...props}
    >
      {children}
      {onRemove && !disabled && (
        <RemoveIcon data-testid="remove-icon" onClick={handleRemove} />
      )}
    </Element>
  );
};
 
export default Chip;