티스토리 뷰

카테고리 없음

보투게더 사용성 개선하기 (3) - 이미지 클릭 시 이미지 자세히 보기

YG - 96년생 , 강아지 있음, 개발자 희망 2023. 10. 25. 17:30

 

보투게더 팀 블로그에서 작성한 글을 가져왔습니다.

 

 

보투게더 사용성 개선하기 (3) - 이미지 클릭 시 이미지 자세히 보기

이미지가 작아서 확대해서 보고 싶은 마음이 들었습니다.다른 사이트에서 이미지를 누르면 보통 확대된 이미지가 나오는데요. 보투게더 사이트에서 사용자가 예상한 동작이 작동하지 않으면

velog.io

 

 

구현하게 된 계기

  1. 이미지가 작아서 확대해서 보고 싶은 마음이 들었습니다.
  2. 다른 사이트에서 이미지를 누르면 보통 확대된 이미지가 나오는데요. 보투게더 사이트에서 사용자가 예상한 동작이 작동하지 않으면 불편함을 느낄 것이라고 생각이 들었습니다.

원하는 동작

  1. 이미지를 누르면 이미지를 자세히 볼 수 있는 확대 창이 나온다.

구현 방법

  1. 웹 접근성을 위해서 dialog 태그로 구현했습니다
  2. dialog를 열고 닫기 위해서는 ref를 이용하기에 forwardRef를 이용해 ref를 인자로 받았습니다
  3. 이미지를 클릭하면 기존의 클릭 이벤트는 막아주었습니다. (보투게더의 경우 선택지 이미지를 클릭 시 투표가 되어서 막아주었습니다)
  4. 이미지 클릭 시 src를 state로 저장하고, dialog를 열어줍니다.
  5. 닫기 버튼 혹은 이미지 밖을 누르면 dialog를 닫아줍니다.

특별히 신경 쓴 점으로는 가로가 긴 이미지가 있을 수도 있고, 세로가 긴 이미지가 있을 수도 있는데 각각 사진 비율에 맞게 보여주기 위해 CSS에 신경을 썼습니다.

ImageZoomModal.tsx

import { ForwardedRef, MouseEvent, forwardRef } from 'react';

import cancel from '@assets/x_mark_black.svg';

import * as S from './style';

interface ImageZoomModalProps {
  src: string;
  handleCloseClick: (event: MouseEvent<HTMLDialogElement>) => void;
  closeZoomModal: () => void;
}

const ImageZoomModal = forwardRef(function ImageZoomModal(
  { src, handleCloseClick, closeZoomModal }: ImageZoomModalProps,
  ref: ForwardedRef<HTMLDialogElement>
) {
  return (
    <S.Dialog
      ref={ref}
      tabIndex={1}
      aria-label="이미지를 확대해서 볼 수 있는 창이 열렸습니다. 이미지 확대 창 닫기 버튼을 누르거나 ESC를 누르면 닫을 수 있습니다."
      aria-modal={true}
      onClick={handleCloseClick}
    >
      <S.Container>
        <S.HiddenCloseButton onClick={closeZoomModal}>이미지 확대 창 닫기</S.HiddenCloseButton>
        <S.CloseButton onClick={closeZoomModal} aria-label="이미지 확대 창 닫기">
          <S.IconImage src={cancel} alt="취소 아이콘" />
        </S.CloseButton>
        <S.Image src={src}></S.Image>
      </S.Container>
    </S.Dialog>
  );
});

export default ImageZoomModal;

ImageZoomModal.styles.ts

import { styled } from 'styled-components';

import { theme } from '@styles/theme';

export const Dialog = styled.dialog`
  position: fixed;

  margin: auto;

  overflow: visible;

  background: none;

  z-index: ${theme.zIndex.modal};

  &::backdrop {
    background-color: rgba(0, 0, 0, 0.35);
  }
`;

export const Container = styled.div`
  position: relative;

  width: 100%;
  height: 100%;
`;

export const HiddenCloseButton = styled.button`
  position: absolute;
  top: 0;
  right: 99999px;
`;

export const CloseButton = styled.button`
  display: flex;
  justify-content: center;
  align-items: center;

  position: absolute;
  top: -50px;
  left: 0;
  right: 0;

  width: fit-content;
  margin: 0 auto;
  padding: 8px;
  border-radius: 50%;

  transition: background-color 0.2s ease-in-out;

  background-color: rgba(255, 255, 255, 0.7);

  cursor: pointer;

  &:hover {
    background-color: rgba(255, 255, 255, 1);
  }
`;

export const IconImage = styled.img`
  width: 24px;
  height: 24px;
`;

export const Image = styled.img`
  width: 100%;
  height: 100%;
  max-height: 80vh;

  object-fit: contain;
`;

useDialog.tsx

import { MouseEvent, useRef } from 'react';

export const useDialog = () => {
  const dialogRef = useRef<HTMLDialogElement>(null);

  const openDialog = () => {
    if (!dialogRef.current) return;

    dialogRef.current.showModal();
  };

  const closeDialog = () => {
    if (!dialogRef.current) return;

    dialogRef.current.close();
  };

  const handleCloseClick = (event: MouseEvent<HTMLDialogElement>) => {
    const modalBoundary = event.currentTarget.getBoundingClientRect();

    if (
      modalBoundary.left > event.clientX ||
      modalBoundary.right < event.clientX ||
      modalBoundary.top > event.clientY ||
      modalBoundary.bottom < event.clientY
    ) {
      closeDialog();
    }
  };

  return { dialogRef, openDialog, closeDialog, handleCloseClick };
};

useImageZoomModal.tsx

import { MouseEvent, useState } from 'react';

import { useDialog } from './useDialog';

export const useImageZoomModal = () => {
  const [imageSrc, setImageSrc] = useState('');
  const { closeDialog, dialogRef, handleCloseClick, openDialog } = useDialog();

  const handleImageClick = (event: MouseEvent<HTMLImageElement>) => {
    event.stopPropagation();
    const src = event.currentTarget.src;
    setImageSrc(src);
    openDialog();
  };

  return {
    imageSrc,
    closeZoomModal: closeDialog,
    handleCloseClick,
    zoomModalRef: dialogRef,
    handleImageClick,
  };
};

사용 방법

function ImageZoomModalStory() {
  const { closeZoomModal, handleCloseClick, handleImageClick, imageSrc, zoomModalRef } =
    useImageZoomModal();

  return (
    <>
      <Container>
        {IMAGE_URL_LIST.map(item => (
          <Image key={item} src={item} onClick={handleImageClick} />
        ))}
      </Container>
      <ImageZoomModal
        src={imageSrc}
        closeZoomModal={closeZoomModal}
        handleCloseClick={handleCloseClick}
        ref={zoomModalRef}
      />
    </>
  );
}

구현 후 이미지 확대 창이 열린 사진

가로가 긴 이미지

세로가 긴 이미지

보투게더 ImageZoomModal 스토리북 링크

https://woowacourse-teams.github.io/2023-votogether/?path=/story/components-common-imagezoommodal--default

 

 
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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
글 보관함