All files / kernel-ui/src/components/shared Modal.tsx

100% Statements 25/25
86.66% Branches 13/15
100% Functions 5/5
100% Lines 25/25

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 106 107 108 109 110 111 112 113 114 115 116 117 118                                                          4x             34x     34x 34x 1x 1x       34x 19x   19x     19x 19x       34x 33x 33x         34x     3x 1x       34x 15x     19x 19x 1x 18x 1x     19x                                                                    
import {
  Box,
  ButtonIcon,
  ButtonIconSize,
  IconName,
  Text as TextComponent,
  TextVariant,
} from '@metamask/design-system-react';
import { useEffect, useRef } from 'react';
 
export type ModalProps = {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
  size?: 'sm' | 'md' | 'lg';
};
 
/**
 * A modal component that displays content in an overlay.
 *
 * @param props - The modal props.
 * @param props.isOpen - Whether the modal is open.
 * @param props.onClose - Function to call when the modal should be closed.
 * @param props.title - The title to display in the modal header.
 * @param props.children - The content to display in the modal body.
 * @param props.size - The size of the modal.
 * @returns A modal component.
 */
export const Modal: React.FC<ModalProps> = ({
  isOpen,
  onClose,
  title,
  children,
  size = 'md',
}) => {
  const modalRef = useRef<HTMLDivElement>(null);
 
  // Handle ESC key to close modal
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent): void => {
      Eif (event.key === 'Escape') {
        onClose();
      }
    };
 
    if (isOpen) {
      document.addEventListener('keydown', handleKeyDown);
      // Prevent body scroll when modal is open
      document.body.style.overflow = 'hidden';
 
      // Focus the modal for accessibility
      Eif (modalRef.current) {
        modalRef.current.focus();
      }
    }
 
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      document.body.style.overflow = 'unset';
    };
  }, [isOpen, onClose]);
 
  // Handle click outside modal to close
  const handleBackdropClick = (
    event: React.MouseEvent<HTMLDivElement>,
  ): void => {
    if (event.target === event.currentTarget) {
      onClose();
    }
  };
 
  if (!isOpen) {
    return null;
  }
 
  let widthClass = 'w-2/3';
  if (size === 'sm') {
    widthClass = 'w-96';
  } else if (size === 'lg') {
    widthClass = 'w-4/5';
  }
 
  return (
    <Box
      className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
      onClick={handleBackdropClick}
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
    >
      <div
        className={`bg-background-default rounded-lg shadow-lg max-h-[90vh] overflow-hidden ${widthClass}`}
        ref={modalRef}
        tabIndex={-1}
      >
        <Box className="flex justify-between items-center bg-alternative p-4 border-b border-muted">
          <TextComponent
            data-testid="modal-title"
            className="!m-0"
            variant={TextVariant.HeadingSm}
          >
            {title}
          </TextComponent>
          <ButtonIcon
            iconName={IconName.Close}
            size={ButtonIconSize.Sm}
            onClick={onClose}
            ariaLabel="Close modal"
            data-testid="modal-close-button"
          />
        </Box>
        <Box className="p-4 overflow-y-auto">{children}</Box>
      </div>
    </Box>
  );
};