UNPKG

@lani.ground/react-modal

Version:
265 lines (264 loc) 13.4 kB
"use strict"; 'use client'; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.useModalContext = exports.ModalProvider = void 0; var jsx_runtime_1 = require("react/jsx-runtime"); var react_1 = require("react"); var ModalRenderer_1 = __importDefault(require("../components/ModalRenderer")); var scroll_1 = require("../utils/scroll"); // Context 생성 var ModalContext = (0, react_1.createContext)(undefined); // Provider 컴포넌트 function ModalProvider(_a) { var _this = this; var children = _a.children; var _b = (0, react_1.useState)([]), modals = _b[0], setModals = _b[1]; var _c = (0, react_1.useState)(false), isMounted = _c[0], setIsMounted = _c[1]; var eventListenersAttached = (0, react_1.useRef)(false); // 클라이언트 마운트 체크 (Next.js App Router SSR 이슈 해결) (0, react_1.useEffect)(function () { setIsMounted(true); return function () { // Provider 언마운트 시 모든 이벤트 리스너 정리 document.removeEventListener('wheel', scroll_1.preventScrollPropagation); document.removeEventListener('touchmove', scroll_1.preventScrollPropagation); }; }, []); // 이벤트 리스너 관리 함수 var manageEventListeners = (0, react_1.useCallback)(function (shouldAttach) { if (shouldAttach && !eventListenersAttached.current) { document.addEventListener('wheel', scroll_1.preventScrollPropagation, { passive: false, }); document.addEventListener('touchmove', scroll_1.preventScrollPropagation, { passive: false, }); eventListenersAttached.current = true; } else if (!shouldAttach && eventListenersAttached.current) { document.removeEventListener('wheel', scroll_1.preventScrollPropagation); document.removeEventListener('touchmove', scroll_1.preventScrollPropagation); eventListenersAttached.current = false; } }, []); // 모달 상태 변경 시 이벤트 리스너 관리 (0, react_1.useEffect)(function () { if (!isMounted) return; var hasNonDisabledScrollLockModal = modals.some(function (modal) { return !modal.disabledScrollLock; }); manageEventListeners(hasNonDisabledScrollLockModal); }, [isMounted, modals, manageEventListeners]); // 모달 열기 - 즉시 처리 var openModal = (0, react_1.useCallback)(function (modal) { // 서버에서는 모달을 열지 않음 if (!isMounted) return ''; // 이미 같은 이름의 모달이 열려있는지 확인 var existingModal = modals.find(function (m) { return m.name === modal.name; }); if (existingModal) { return existingModal.id; // 이미 열려있는 모달의 ID 반환 } var id = "modal-".concat(Date.now(), "-").concat(Math.random() .toString(36) .substr(2, 9)); var newModal = __assign(__assign({}, modal), { id: id }); setModals(function (prev) { return __spreadArray(__spreadArray([], prev, true), [newModal], false); }); return id; }, [isMounted, modals]); // 모달 닫기 - 즉시 처리 var closeModal = (0, react_1.useCallback)(function (id) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { // 서버에서는 처리하지 않음 if (!isMounted) return [2 /*return*/, Promise.resolve()]; return [2 /*return*/, new Promise(function (resolve) { // 먼저 닫히는 중 상태로 변경하고 애니메이션 시간 저장 var animationDuration = 0; setModals(function (prevModals) { var _a; var targetModal = prevModals.find(function (modal) { return modal.id === id; }); if (!targetModal) { resolve(); return prevModals; } // 애니메이션 시간 저장 animationDuration = ((_a = targetModal.animation) === null || _a === void 0 ? void 0 : _a.duration) || 0; // 모달 닫기 시작도 즉시 반영 (사용자가 즉시 봐야 함) return prevModals.map(function (modal) { return modal.id === id ? __assign(__assign({}, modal), { isClosing: true }) : modal; }); }); // 애니메이션 시간만큼 기다린 후 제거 var cleanup = function () { setModals(function (prevModals) { var modalToRemove = prevModals.find(function (m) { return m.id === id; }); if (modalToRemove === null || modalToRemove === void 0 ? void 0 : modalToRemove.onClose) { try { modalToRemove.onClose(); } catch (error) { console.warn('Modal onClose callback error:', error); } } resolve(); return prevModals.filter(function (m) { return m.id !== id; }); }); }; if (animationDuration > 0) { setTimeout(cleanup, animationDuration); } else { cleanup(); } })]; }); }); }, [isMounted]); // 모든 모달 닫기 - 즉시 처리 var closeAllModals = (0, react_1.useCallback)(function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { if (!isMounted) return [2 /*return*/, Promise.resolve()]; return [2 /*return*/, new Promise(function (resolve) { setModals(function (prevModals) { prevModals.forEach(function (modal) { if (modal.onClose) { try { modal.onClose(); } catch (error) { console.warn('Modal onClose callback error:', error); } } }); resolve(); return []; }); // 모든 모달이 닫힐 때 이벤트 리스너도 정리 manageEventListeners(false); })]; }); }); }, [isMounted, manageEventListeners]); // 특정 모달이 열려있는지 확인 var isModalOpen = (0, react_1.useCallback)(function (id) { // 서버에서는 항상 false 반환 if (!isMounted) return false; return modals.some(function (modal) { return modal.id === id; }); }, [modals, isMounted]); // 모든 라우팅 변경 감지 (popstate + 프로그래매틱 라우팅) (0, react_1.useEffect)(function () { if (typeof window === 'undefined' || !isMounted) return; // 현재 pathname 저장 var currentPathname = window.location.pathname; var closeModalsOnRouteChange = function () { var newPathname = window.location.pathname; if (newPathname !== currentPathname) { currentPathname = newPathname; // closeAllModals를 사용하여 모든 cleanup 로직이 실행되도록 함 closeAllModals(); } }; // 1. 브라우저 뒤로가기/앞으로가기 감지 window.addEventListener('popstate', closeModalsOnRouteChange); // 2. 프로그래매틱 라우팅 감지 (next/link, react-router 등) var originalPushState = window.history.pushState; var originalReplaceState = window.history.replaceState; // pushState 오버라이드 (next/link, router.push 등) window.history.pushState = function (state, title, url) { var result = originalPushState.call(this, state, title, url); // 다음 마이크로태스크에서 체크 (DOM 업데이트 후) Promise.resolve().then(closeModalsOnRouteChange); return result; }; // replaceState 오버라이드 (router.replace 등) window.history.replaceState = function (state, title, url) { var result = originalReplaceState.call(this, state, title, url); Promise.resolve().then(closeModalsOnRouteChange); return result; }; // 정리 함수 return function () { window.removeEventListener('popstate', closeModalsOnRouteChange); window.history.pushState = originalPushState; window.history.replaceState = originalReplaceState; }; }, [isMounted, closeAllModals]); // closeAllModals 의존성 추가 var value = { modals: modals, openModal: openModal, closeModal: closeModal, closeAllModals: closeAllModals, isModalOpen: isModalOpen, }; return ((0, jsx_runtime_1.jsxs)(ModalContext.Provider, { value: value, children: [children, isMounted && (0, jsx_runtime_1.jsx)(ModalRenderer_1.default, {})] })); } exports.ModalProvider = ModalProvider; // Custom Hook function useModalContext() { var context = (0, react_1.useContext)(ModalContext); if (context === undefined) { throw new Error('useModalContext must be used within a ModalProvider'); } return context; } exports.useModalContext = useModalContext;