본문 바로가기
펀잇

[2탄] dialog 위에 토스트 뜨게 하기

by 해-온 2024. 5. 7.

 

 

 

펀잇에서 dialog 위에 토스트가 가려 보이지 않는 문제가 있었다.

엄~청 삽질하다가 결국 div에 role = 'dialog'를 주는 방식으로 해결했는데...

 

(자세한 내용은 아래에 있다)

 

[🐛트러블슈팅] 토스트를 구웠는데 먹을 수 없다구요?!?!

dialog에서 토스트가 가려져요.... '펀잇'에서 리뷰를 작성할 때 각종 알람이 뜬다. 예를 들어 사용자가 이미지를 등록한다고 생각해 보자. 5MB 이하의 이미지만 받고 있다고 할 때 그보다 큰 용량의

hae-on.tistory.com

 

어찌어찌 해결은 했지만, 뭔가 만족스럽지는 않은 해결법이었다.

dialog 태그를 꼭 사용하고 싶었기 때문!

dialog 태그를 사용해야 웹 접근성 측면에서 효율적이기 때문이다.

 

그래서 사용할 수 있는 다른 방법을 찾으면 적용해보자 싶었는데

'보투게더' 팀에서 올린 포스트가 있어 이를 참고해 수정해 보았다.

 

 

Dialog 태그 위로 토스트 보이도록 하기 (feat.TopLayer, createPortal)

html dialog 태그로 만든 Drawer에서 에러가 났을 때 토스트가 보이지 않는 상황이 있었습니다. 이유는 dialog는 최상위 계층 (Top layer)으로 열리기 때문인데요. topLayer는 페이지의 다른 모든 콘텐츠 레

velog.io

 

 

간단하게 축약하자면,

토스트의 id를 동적으로 바꾸는 것이다.

dialog가 있으면 토스트가 dialog 내부에 붙게 하고,

없으면 원래 있던 위치에 붙게 하는 것이다.

 

const BottomSheet = (
  { maxWidth, maxHeight, isClosing, close, hasToast, children, ...props }: BottomSheetProps,
  ref: ForwardedRef<HTMLDialogElement>
) => {
  return createPortal(
    <ModalDialog ref={ref} {...props}>
      <BackDrop onClick={close} />
      <ModalWrapper maxWidth={maxWidth} isClosing={isClosing}>
        {children}
      </ModalWrapper>
      {hasToast && <div id="toast-in-dialog-container" aria-hidden />} //여기 추가
    </ModalDialog>,
    containerElement
  );
};

export default forwardRef(BottomSheet);

 

 

좀 잘라먹긴 했는데 아래쪽에 보면 

 

{hasToast && <div id="toast-in-dialog-container" aria-hidden />}

 

요 부분을 추가해 준다.

 

dialog랑 상관없는 경우에는 원래 위치의 portal id에 따라 토스트가 붙어야 하기 때문에

dialog에서 사용할 때만 hasToast라는 props를 따로 받도록 했다.

 

그래서 

 

<BottomSheet hasToast isClosing={isClosing} close={handleCloseBottomSheet} ref={ref}>

 

요렇게 dialog에 hasToast props를 주면,

토스트가 toast-in-dialog-container에 붙게 되는 것이다.

 

 

그리고 토스트에서도 id를 받아서 변경해줘야 한다.

아래는 전체 토스트 context 코드이다.

 

export interface ToastState {
  id: number;
  message: string;
  isError?: boolean;
}

export interface ToastValue {
  toasts: ToastState[];
}

export interface ToastAction {
  toast: {
    success: (message: string) => void;
    error: (message: string) => void;
  };
  deleteToast: (id: number) => void;
  setToastId: (id: ToastId) => void;
}

export const ToastValueContext = createContext<ToastValue | null>(null);
export const ToastActionContext = createContext<ToastAction | null>(null);

export const ToastProvider = ({ children }: PropsWithChildren) => {
  const [toasts, setToasts] = useState<ToastState[]>([]);
  const [toastElementId, setToastElementId] = useState<ToastId>('toast-container');

  const showToast = (id: number, message: string, isError?: boolean) => {
    setToasts([...toasts, { id, message, isError }]);
  };
  const deleteToast = (id: number) => {
    setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id));
  };

  const setToastId = (id: ToastId) => {
    setToastElementId(id);
  };

  const toast = {
    success: (message: string) => showToast(Number(Date.now()), message),
    error: (message: string) => showToast(Number(Date.now()), message, true),
  };
  const toastValue = {
    toasts,
  };
  const toastAction = {
    toast,
    deleteToast,
    setToastId,
  };

  return (
    <ToastActionContext.Provider value={toastAction}>
      <ToastValueContext.Provider value={toastValue}>
        {children}
        {createPortal(
          <ToastContainer>
            {toasts.map(({ id, message, isError }) => (
              <Toast key={id} id={id} message={message} isError={isError} />
            ))}
          </ToastContainer>,
          document.getElementById(toastElementId) as HTMLElement
        )}
      </ToastValueContext.Provider>
    </ToastActionContext.Provider>
  );
};
export default ToastProvider;

 

 

이렇게 기존 토스트 id 값으로 상태를 만들어주고,

 

const [toastElementId, setToastElementId] = useState<ToastId>('toast-container');

 

 

토스트 아이디를 바꿔주는 함수를 만든다.

 

const setToastId = (id: ToastId) => {
    setToastElementId(id);
  };

 

 

그리고 마지막으로 기존에는 toast-container라는 주어진 값으로 받던걸

 

document.getElementById('toast-container') as HTMLElement

 

 

아래처럼 상태로 받게 하면 끗 -!

 

return (
    <ToastActionContext.Provider value={toastAction}>
      <ToastValueContext.Provider value={toastValue}>
        {children}
        {createPortal(
          <ToastContainer>
            {toasts.map(({ id, message, isError }) => (
              <Toast key={id} id={id} message={message} isError={isError} />
            ))}
          </ToastContainer>,
          document.getElementById(toastElementId) as HTMLElement //여기
        )}
      </ToastValueContext.Provider>
    </ToastActionContext.Provider>
  );
};

 

마지막으로 dialog를 열고 닫을 때마다 id를 바꿔준다.

 

const { setToastId } = useToastActionContext();

const handleOpenBottomSheet = () => {
    setToastId('toast-in-dialog-container');
    ref.current?.showModal();
  };

  const handleCloseBottomSheet = () => {
    setToastId('toast-container');
    closeAnimated();
  };

 

 

전체 코드는 아래와 같다.

div로 하던 걸 dialog 태그로 다시 바꾼 거라

열고 닫는 로직까지 다 변경되어 있어 좀 헷갈릴 수 있지만...!

 

 

refactor: Dialog 레이어 쌓임 문제 해결 by hae-on · Pull Request #96 · fun-eat/design-system

Issue close #95 ✨ 구현한 기능 여러분 드디어 고쳤습니다...! toast id를 따로 상태로 둬서 일반 상황에서는 'toast-container'라는 div에 붙고 dialog 내부에서는 'toast-in-dialog-container' div에 붙도록 하였습니

github.com

 

 

 

댓글