@lani.ground/react-modal
Version:
Modal components used in reactjs
265 lines (264 loc) • 13.4 kB
JavaScript
;
'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;