펀잇에서 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
'펀잇' 카테고리의 다른 글
compound component pattern을 활용해 컴포넌트 조립하기 (0) | 2024.08.20 |
---|---|
Meta OG image 교체가 안 된다면?! (1) | 2024.07.23 |
펀잇 리뷰 작성 폼에서 웹 접근성 개선하기 (2) | 2023.11.12 |
최종 데모데이 후기 (2) | 2023.11.05 |
4, 5차 데모데이 피드백과 반영 (0) | 2023.11.01 |
댓글