diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5fee3b4f..e95589c5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -24,7 +24,9 @@ export default function RootLayout({ - {children} +
+ {children} +
diff --git a/src/components/pages/profile/profile-edit-modal/index.tsx b/src/components/pages/profile/profile-edit-modal/index.tsx index c55aa8c2..2e1ecceb 100644 --- a/src/components/pages/profile/profile-edit-modal/index.tsx +++ b/src/components/pages/profile/profile-edit-modal/index.tsx @@ -41,13 +41,12 @@ export const ProfileEditModal = ({ user }: Props) => { }); return ( - + 프로필 수정 이 모달은 자신의 프로필을 수정할 수 있는 모달입니다.
{ e.preventDefault(); e.stopPropagation(); diff --git a/src/components/ui/imageinput/index.tsx b/src/components/ui/imageinput/index.tsx index 52071a33..e2f81b98 100644 --- a/src/components/ui/imageinput/index.tsx +++ b/src/components/ui/imageinput/index.tsx @@ -121,6 +121,7 @@ export const ImageInput = ({ style={{ display: 'none' }} accept={accept} multiple={multiple} + tabIndex={-1} type='file' onChange={handleFileChange} /> diff --git a/src/components/ui/modal/index.tsx b/src/components/ui/modal/index.tsx index c1258cf7..5b7dff75 100644 --- a/src/components/ui/modal/index.tsx +++ b/src/components/ui/modal/index.tsx @@ -2,6 +2,8 @@ import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; +import * as m from 'motion/react-m'; + import { Icon } from '@/components/icon'; import { cn } from '@/lib/utils'; @@ -23,12 +25,16 @@ interface ModalProviderProps { } export const ModalProvider = ({ children }: ModalProviderProps) => { + const [mounted, setMounted] = useState(false); const [isOpen, setIsOpen] = useState(false); const [content, setContent] = useState(null); const previousFocusRef = useRef(null); const lastInputTypeRef = useRef<'mouse' | 'keyboard'>('mouse'); + const modalWrapperRef = useRef(null); + const isMouseDownInsideModal = useRef(false); + const open = (modalContent: React.ReactNode) => { setContent(modalContent); setIsOpen(true); @@ -43,11 +49,16 @@ export const ModalProvider = ({ children }: ModalProviderProps) => { setContent(null); setIsOpen(false); if (previousFocusRef.current) { - previousFocusRef.current.focus(); + const el = previousFocusRef.current; + setTimeout(() => { + el.focus(); + }, 0); + previousFocusRef.current = null; } }; + // Modal을 Open 할 때 키보드로 진입했다면 Trigger 요소를 기억함 useEffect(() => { const handleMouseDown = () => { lastInputTypeRef.current = 'mouse'; @@ -65,6 +76,37 @@ export const ModalProvider = ({ children }: ModalProviderProps) => { }; }, []); + // Modal 외부 Mousedown => 내부 MouseUp 일 때 Modal이 닫히지 않음 + // Modal 내부 Mousedown => 외부 MouseUp 일 때 Modal이 닫히지 않음 + // Modal 외부 Mousedown => 외부 Mouseup 일 때 Modal 닫힘 + useEffect(() => { + if (!isOpen) return; + const handleMouseDown = (e: MouseEvent) => { + if (modalWrapperRef.current?.contains(e.target as Node)) { + isMouseDownInsideModal.current = true; + } else { + isMouseDownInsideModal.current = false; + } + }; + + const handleMouseUp = (e: MouseEvent) => { + if ( + !modalWrapperRef.current?.contains(e.target as Node) && + isMouseDownInsideModal.current === false + ) { + close(); + } + }; + + document.addEventListener('mousedown', handleMouseDown); + document.addEventListener('mouseup', handleMouseUp); + + return () => { + document.removeEventListener('mousedown', handleMouseDown); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [isOpen]); + // esc 입력 시 Modal close useEffect(() => { const handleEscape = (e: KeyboardEvent) => { @@ -89,7 +131,7 @@ export const ModalProvider = ({ children }: ModalProviderProps) => { // Modal Open 상태일 때 배경 요소들 무시 useEffect(() => { if (!isOpen) return; - const appRoot = document.getElementById('__next') || document.getElementById('root'); + const appRoot = document.getElementById('root'); if (appRoot) { appRoot.setAttribute('inert', ''); appRoot.setAttribute('aria-hidden', 'true'); @@ -102,20 +144,47 @@ export const ModalProvider = ({ children }: ModalProviderProps) => { }; }, [isOpen]); + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true); + }, []); + return ( {children} - {isOpen && content} + {mounted && + createPortal( + , + document.body, + )} ); }; interface ModalContentProps { children: React.ReactNode; + className?: string; } -export const ModalContent = ({ children }: ModalContentProps) => { - const { close } = useModal(); +export const ModalContent = ({ children, className }: ModalContentProps) => { const modalRef = useRef(null); // focus 처리 @@ -124,7 +193,7 @@ export const ModalContent = ({ children }: ModalContentProps) => { const modal = modalRef.current; const focusableElements = modal.querySelectorAll( - 'button:not([disabled]), a[href]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + 'button:not([disabled]), a[href]:not([tabindex="-1"]), input:not([disabled]):not([tabindex="-1"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', ); const firstElement = focusableElements[0] as HTMLElement; const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement; @@ -158,29 +227,24 @@ export const ModalContent = ({ children }: ModalContentProps) => { return () => modal.removeEventListener('keydown', handleTab); }, [children]); - return createPortal( -
{ + e.stopPropagation(); + }} > -
{ - e.stopPropagation(); - }} - > -
- {children} - -
+
+ {children} +
-
, - document.body, + ); };