Skip to content

Latest commit

ย 

History

History
579 lines (410 loc) ยท 16.3 KB

README-ko.md

File metadata and controls

579 lines (410 loc) ยท 16.3 KB

@reactleaf/modal

npm version

React ์ฝ”๋“œ ์–ด๋””์—์„œ๋‚˜ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์—ด๊ณ , ๋‹ซํž ๋•Œ์˜ ๊ฒฐ๊ณผ๋ฅผ Promise๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ํƒ€์ž…-์„ธ์ดํ”„ํ•œ ๋ชจ๋‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

๊ฐœ์š”

@reactleaf/modal์€ ์ฝ”๋“œ ์–ด๋””์—์„œ๋‚˜ ๋ชจ๋‹ฌ์„ ์—ด๊ณ , ์—ฌ๋Ÿฌ ๋ชจ๋‹ฌ์„ ์ค‘์ฒฉํ•ด์„œ ๋„์šธ ์ˆ˜ ์žˆ๋Š” React ๋ชจ๋‹ฌ ๋งค๋‹ˆ์ €์ž…๋‹ˆ๋‹ค. ์ด๋ฏธ ์—ด๋ฆฐ ๋ชจ๋‹ฌ ์œ„์— ํ™•์ธ, ์ž…๋ ฅ, ๊ฒฝ๊ณ  ๋ชจ๋‹ฌ์„ ๋‹ค์‹œ ์—ด ์ˆ˜ ์žˆ๊ณ , ๊ฐ ๋ชจ๋‹ฌ์€ ์—ด๋ฆฐ ์ˆœ์„œ๋Œ€๋กœ stack์— ์Œ“์ž…๋‹ˆ๋‹ค.

v2์—์„œ ๋ฐ”๋€ ์ 

@reactleaf/modal v2๋Š” v1์˜ register ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

์•ฑ์—์„œ ModalManager ์ธ์Šคํ„ด์Šค๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค๊ณ , ๊ทธ ์ธ์Šคํ„ด์Šค๋ฅผ ModalProvider์— ์—ฐ๊ฒฐํ•œ ๋’ค, ํ•„์š”ํ•œ ๊ณณ์—์„œ modal.open(Component, props, options)๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋‹ฌ ๋‚ด๋ถ€์—์„œ๋Š” useModalInstance()๋กœ ํ˜„์žฌ ๋ชจ๋‹ฌ์˜ ํ‘œ์‹œ ์ƒํƒœ, ์ž๊ธฐ ์ž์‹ ์„ ๋‹ซ๋Š” ํ•จ์ˆ˜, ์ž๊ธฐ ์ž์‹ ์„ ๋‹ค์Œ ๋ชจ๋‹ฌ๋กœ ๊ต์ฒดํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐํ•˜๋ ค๋˜ ๋ฌธ์ œ๋“ค

  • ๋ฌธ์ž์—ด ์ด๋ฆ„์ด๋‚˜ register ํŒŒ์ผ ์—†์ด ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์—ฝ๋‹ˆ๋‹ค. SSR ๊ณผ์ •์—์„œ import๊ฐ€ ํŒŒ์•…๋˜์ง€ ์•Š์•„ ์Šคํƒ€์ผ์„ ์ถ”์ถœํ•  ์ˆ˜ ์—†๋˜ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
  • Browser Back button์„ ๋ˆŒ๋ €์„ ๋•Œ ๋ชจ๋‹ฌ์ด ๋‹ซํžˆ๋Š” ๋™์ž‘์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก, ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. (ํŠนํžˆ, Android ๋””๋ฐ”์ด์Šค์—์„œ)

๋ณ€๊ฒฝ์‚ฌํ•ญ

  • register.ts๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • useModal()๊ณผ createModalHook()์ด ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๋ฌธ์ž์—ด ๊ธฐ๋ฐ˜ ๋ชจ๋‹ฌ ์ด๋ฆ„ ๋Œ€์‹  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  • BasicModalProps๊ฐ€ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • modal.open(Component, props?, options?)๋Š” ๋ชจ๋‹ฌ์ด ๋‹ซํž ๋•Œ resolve๋˜๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ๋Š” useModalInstance()๋กœ visible, closeSelf, replaceSelf๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค.

์„ค์น˜

npm install @reactleaf/modal
# ๋˜๋Š”
yarn add @reactleaf/modal
# ๋˜๋Š”
pnpm add @reactleaf/modal

๊ธฐ๋ณธ ๋ ˆ์ด์–ด์™€ dim ์Šคํƒ€์ผ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์•ฑ ์—”ํŠธ๋ฆฌ์—์„œ ์Šคํƒ€์ผ์‹œํŠธ๋ฅผ ํ•œ ๋ฒˆ importํ•ฉ๋‹ˆ๋‹ค.

import "@reactleaf/modal/style.css";

๋น ๋ฅธ ์‹œ์ž‘

1. manager ์ธ์Šคํ„ด์Šค ๋งŒ๋“ค๊ธฐ

ModalProvider์™€ modal.open(...) ํ˜ธ์ถœ๋ถ€๋Š” ๊ฐ™์€ ModalManager ์ธ์Šคํ„ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ณดํ†ต ๋ณ„๋„ ํŒŒ์ผ์—์„œ ํ•˜๋‚˜๋งŒ ๋งŒ๋“ค๊ณ  exportํ•ฉ๋‹ˆ๋‹ค.

// modal.ts
import { ModalManager } from "@reactleaf/modal";

export const modal = new ModalManager();

2. Provider ์—ฐ๊ฒฐํ•˜๊ธฐ

๋งŒ๋“ค์–ด์ง„ Manager๋ฅผ Provider์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

import { ModalProvider } from "@reactleaf/modal";
import { modal } from "./modal";

function App() {
  return (
    <ModalProvider
      manager={modal}
      defaultLayerOptions={{ closeDelay: 180, closeOnOutsideClick: true, dim: true }}
      rootOptions={{ preventScroll: true }}
    >
      <YourApp />
    </ModalProvider>
  );
}

ModalProvider๋Š” children์„ ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋งํ•˜๋ฉด์„œ, ์—ด๋ฆฐ ๋ชจ๋‹ฌ๋“ค์„ ์œ„ํ•œ ๋ ˆ์ด์–ด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํ•จ๊ป˜ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

3. ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ

๋ชจ๋‹ฌ์€ ์ผ๋ฐ˜ React ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. visible์€ ์—ด๋ฆผ/๋‹ซํž˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒํƒœ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , closeSelf(result)๋Š” ํ˜„์žฌ ๋ชจ๋‹ฌ์„ ๋‹ซ์œผ๋ฉด์„œ modal.open(...)์˜ Promise๋ฅผ result๋กœ resolveํ•ฉ๋‹ˆ๋‹ค. useModalInstance<Result>()์— ์ „๋‹ฌํ•˜๋Š” ์ œ๋„ค๋ฆญ์€ closeSelf(...)๊ฐ€ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๊ฐ’์˜ ํƒ€์ž…์„ ์ œํ•œํ•˜๋Š” ์šฉ๋„์ž…๋‹ˆ๋‹ค.

import { ModalComponent, useModalInstance } from "@reactleaf/modal";

export type ConfirmProps = {
  message: string;
};

export const Confirm: ModalComponent<ConfirmProps, boolean> = ({ message }) => {
  const { visible, closeSelf } = useModalInstance<boolean>();

  return (
    <div className={visible ? "confirm visible" : "confirm"}>
      <p>{message}</p>
      <button onClick={() => closeSelf(false)}>์ทจ์†Œ</button>
      <button onClick={() => closeSelf(true)}>ํ™•์ธ</button>
    </div>
  );
};

// ์ด ํƒ€์ž… ๋ชจ๋‹ฌ์—๋งŒ ์ ์šฉ๋˜๋Š” ํŠน์ •ํ•œ ์˜ต์…˜์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Confirm.layerOptions = {
  closeOnOutsideClick: false,
};

4. ๋ชจ๋‹ฌ ์—ด๊ณ  ๊ฒฐ๊ณผ ๋ฐ›๊ธฐ

import { modal } from "./modal";
import Confirm from "./modals/Confirm";

async function deleteItem() {
  const confirmed = await modal.open(Confirm, {
    message: "์ •๋ง ์‚ญ์ œํ• ๊นŒ์š”?",
  });

  if (!confirmed) return;

  await requestDelete();
}

API

new ModalManager()

๋ชจ๋‹ฌ stack์„ ๊ด€๋ฆฌํ•˜๋Š” controller ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

import { ModalManager } from "@reactleaf/modal";

export const modal = new ModalManager();

ํ•œ ์•ฑ์—์„œ ์—ฌ๋Ÿฌ manager๋ฅผ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ์–ด๋–ค ModalProvider์— ์—ฐ๊ฒฐ๋œ manager์ธ์ง€ ํ˜ธ์ถœ๋ถ€๊ฐ€ ๋ช…ํ™•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

<ModalProvider manager={...} />

ํŠน์ • manager์˜ ๋ชจ๋‹ฌ stack์„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

<ModalProvider
  manager={modal}
  defaultLayerOptions={{ closeDelay: 180, dim: true }}
  rootOptions={{ preventScroll: true }}
>
  <App />
</ModalProvider>

Props:

  • manager: ModalManager
  • defaultLayerOptions?: Partial<LayerOptions>
  • rootOptions?: Partial<RootOptions>
  • children: React.ReactNode

modal.open(Component, props?, options?)

๋ชจ๋‹ฌ์„ ์—ด๊ณ , ๋‹ซํž ๋•Œ resolve๋˜๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

const result = await modal.open(Alert, {
  message: "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
});

ํ•„์ˆ˜ props๊ฐ€ ์žˆ๋Š” ๋ชจ๋‹ฌ์€ ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ props๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. props๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋ชจ๋‘ optional์ด๋ฉด ๋‘ ๋ฒˆ์งธ ์ธ์ž๋ฅผ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. props๊ฐ€ ์—†๋Š” ๋ชจ๋‹ฌ์— options๋งŒ ์ „๋‹ฌํ•˜๋ ค๋ฉด ๋‘ ๋ฒˆ์งธ ์ธ์ž์— null์„ ๋„ฃ์Šต๋‹ˆ๋‹ค.

await modal.open(EmptyModal);
await modal.open(EmptyModal, null, { closeOnOutsideClick: false });

๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ModalComponent<Props, Result>๋กœ ํƒ€์ž… ์ง€์ •๋˜์–ด ์žˆ์œผ๋ฉด modal.open(...)์€ props์™€ ๊ฒฐ๊ณผ ํƒ€์ž…์„ ํ•จ๊ป˜ ์ถ”๋ก ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž… ์ง€์ •๋˜์ง€ ์•Š์€ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ฒฐ๊ณผ ํƒ€์ž…์„ ์ง์ ‘ ๋ช…์‹œํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” ๋‘ ๋ฒˆ์งธ ์ œ๋„ค๋ฆญ ์ธ์ž๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ๋ฐ˜ํ™˜ ํƒ€์ž…์—๋Š” ์‚ฌ์šฉ์ž ์ง€์ • ๊ฒฐ๊ณผ ํƒ€์ž…์— ๋”ํ•ด ModalAborted | ModalReplaced | undefined๊ฐ€ ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const name = await modal.open(Prompt, {
  title: "์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”.",
});

Resolve ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • ๋ชจ๋‹ฌ ๋‚ด๋ถ€์—์„œ ๋‹ซ๋Š” ํ”Œ๋กœ์šฐ
    • closeSelf(value) -> value
    • closeSelf() -> undefined
  • ๋ชจ๋‹ฌ ์™ธ๋ถ€์—์„œ ๋‹ซ์„ ๋•Œ
    • ๋ฐ”๊นฅ ์˜์—ญ ํด๋ฆญ / Escape / ๋ธŒ๋ผ์šฐ์ € ๋’ค๋กœ๊ฐ€๊ธฐ -> undefined
    • modal.closeWithResult(id, value) -> value
    • modal.close(id) / modal.closeTop() / modal.closeAll() -> undefined
  • ๋ณ„๋„์˜ Abort Controller๋ฅผ ํ†ตํ•ด ๋‹ซ๋Š” ๊ฒฝ์šฐ
    • abortController.abort() -> MODAL_ABORTED
  • replaceSelf(...)๋กœ ๊ต์ฒด๋˜๋Š” ๊ฒฝ์šฐ
    • ๊ต์ฒด๋˜๋Š” ์ด์ „ ๋ชจ๋‹ฌ์˜ open() Promise -> MODAL_REPLACED

modal.closeWithResult(id, result, options?)

์ง€์ •ํ•œ id์˜ ๋ชจ๋‹ฌ์„ ๋‹ซ๊ณ , ํ•ด๋‹น open() Promise๋ฅผ result๋กœ resolveํ•ฉ๋‹ˆ๋‹ค.

modal.closeWithResult(id, { confirmed: true });
modal.closeWithResult(id, { confirmed: true }, { historyBack: true });

modal.close(id, options?)

์ง€์ •ํ•œ id์˜ ๋ชจ๋‹ฌ์„ ๋‹ซ๊ณ , ํ•ด๋‹น open() Promise๋ฅผ undefined๋กœ resolveํ•ฉ๋‹ˆ๋‹ค.

modal.close(id);

modal.closeTop(options?)

๊ฐ€์žฅ ์œ„์— ์žˆ๋Š” ๋ชจ๋‹ฌ์„ ๋‹ซ์Šต๋‹ˆ๋‹ค.

modal.closeTop();
modal.closeTop({ historyBack: true });

closeWithResult(id, result), close(id), closeTop() ๋“ฑ์˜ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ์š”์ฒญ ํ•จ์ˆ˜๋Š” ๋‹ซ์„ ์ˆ˜ ์žˆ๋Š” ๋ชจ๋‹ฌ์ด ์—†๋Š” ๊ฒฝ์šฐ ์•„๋ฌด ์ผ๋„ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

modal.closeAll(options?)

ํ˜„์žฌ ์—ด๋ ค ์žˆ๋Š” ๋ชจ๋“  ๋ชจ๋‹ฌ์„ ๋‹ซ์Šต๋‹ˆ๋‹ค.

modal.closeAll();

modal.hasOpenModals()

์—ด๋ ค ์žˆ๋Š” ๋ชจ๋‹ฌ์ด ํ•˜๋‚˜๋ผ๋„ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

if (modal.hasOpenModals()) {
  console.log("๋ชจ๋‹ฌ์ด ์—ด๋ ค ์žˆ์Šต๋‹ˆ๋‹ค.");
}

modal.getSnapshot()

ํ˜„์žฌ ๋ชจ๋‹ฌ stack์˜ ์ฝ๊ธฐ ์ „์šฉ ์Šค๋ƒ…์ƒท์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

const opened = modal.getSnapshot();

if (opened.some((item) => item.Component === Confirm)) {
  console.log("Confirm์ด ์ด๋ฏธ ์—ด๋ ค ์žˆ์Šต๋‹ˆ๋‹ค.");
}

์Šค๋ƒ…์ƒท ํ•ญ๋ชฉ์—๋Š” id, Component, props, options๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

modal.subscribe(listener)

๋ชจ๋‹ฌ stack ๋ณ€ํ™”๋ฅผ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ ์‚ฌ์šฉ์—์„œ๋Š” ํ•„์š”ํ•˜์ง€ ์•Š์ง€๋งŒ, ๋””๋ฒ„๊น… ํŒจ๋„์ด๋‚˜ ์™ธ๋ถ€ ์ƒํƒœ ์—ฐ๋™์ฒ˜๋Ÿผ stack์„ ๊ด€์ฐฐํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const unsubscribe = modal.subscribe((stack) => {
  console.log("์—ด๋ฆฐ ๋ชจ๋‹ฌ ์ˆ˜:", stack.length);
});

unsubscribe();

useModalInstance()

๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ํ˜„์žฌ ๋ชจ๋‹ฌ ์ธ์Šคํ„ด์Šค์˜ ์ƒํƒœ์™€ ๋‹ซ๊ธฐ ํ•จ์ˆ˜๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค.

const { visible, closeSelf, replaceSelf } = useModalInstance();

์ œ๊ณต ๊ฐ’:

  • visible: boolean
  • closeSelf(result?): Promise<void>
  • replaceSelf(Component, props?, options?): Promise

useModalInstance()๋Š” ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, modal.open(...)์œผ๋กœ ์—ด๋ฆฐ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

replaceSelf(Component, props?, options?)

ํ˜„์žฌ ๋ชจ๋‹ฌ ์ธ์Šคํ„ด์Šค์˜ layer๋ฅผ ์œ ์ง€ํ•œ ์ฑ„ content๋งŒ ์ƒˆ ๋ชจ๋‹ฌ๋กœ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค.

import { useModalInstance } from "@reactleaf/modal";
import CodeModal from "./CodeModal";

function EmailModal({ onVerified }: EmailModalProps) {
  const { replaceSelf } = useModalInstance();

  async function submitEmail(email: string) {
    await sendVerificationCode(email);

    const verified = await replaceSelf(CodeModal, {
      email,
    });

    if (verified) {
      await onVerified();
    }
  }
}

replaceSelf(...)๋Š” ์ƒˆ ๋ชจ๋‹ฌ์ด ๋‹ซํž ๋•Œ ๊ทธ ๊ฒฐ๊ณผ๋กœ resolve๋ฉ๋‹ˆ๋‹ค. ๊ต์ฒด๋˜๋Š” ์ด์ „ ๋ชจ๋‹ฌ์˜ open() Promise๋Š” MODAL_REPLACED๋กœ resolve๋ฉ๋‹ˆ๋‹ค.

๋ชจ๋‹ฌ ์˜ต์…˜

export interface LayerOptions {
  className?: string;
  closeDelay?: number;
  closeOnOutsideClick?: boolean;
  dim?: boolean | string;
}

export interface RootOptions {
  preventScroll?: boolean;
}

export interface ModalOptions extends LayerOptions {
  abortController?: AbortController;
}

์˜ต์…˜ ์šฐ์„ ์ˆœ์œ„

๋ ˆ์ด์–ด ์˜ต์…˜์€ ์•„๋ž˜ ์ˆœ์„œ๋กœ ๋ณ‘ํ•ฉ๋ฉ๋‹ˆ๋‹ค. ๋’ค์˜ ๊ฐ’์ด ์•ž์˜ ๊ฐ’์„ ๋ฎ์–ด์”๋‹ˆ๋‹ค.

  1. ModalProvider์˜ defaultLayerOptions
  2. ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ์˜ layerOptions
  3. modal.open(...) ํ˜ธ์ถœ ์‹œ ์ „๋‹ฌํ•œ options
<ModalProvider manager={modal} defaultLayerOptions={{ closeOnOutsideClick: true, dim: true }}>
  <App />
</ModalProvider>;

Confirm.layerOptions = {
  closeOnOutsideClick: false,
};

await modal.open(Confirm, { message: "์ง„ํ–‰ํ• ๊นŒ์š”?" }, { className: "confirm-layer" });

์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ณธ ์˜ต์…˜

ModalComponent<Props, Result> ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ์— layerOptions๋ฅผ ์„ ์–ธํ•˜๊ณ , modal.open(...)์ด ์ถ”๋ก ํ•  ๊ฒฐ๊ณผ ํƒ€์ž…์„ ํ•จ๊ป˜ ๋…ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { ModalComponent } from "@reactleaf/modal";

type AlertProps = {
  message: string;
};

const Alert: ModalComponent<AlertProps> = ({ message }) => {
  return <div>{message}</div>;
};

Alert.layerOptions = {
  closeOnOutsideClick: false,
};

Promise ๊ธฐ๋ฐ˜ ์‚ฌ์šฉ ์˜ˆ์‹œ

ํ™•์ธ ๋ชจ๋‹ฌ

const confirmed = await modal.open(Confirm, {
  message: "์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?",
});

if (confirmed) {
  await deleteItem();
}

์ž…๋ ฅ ๋ชจ๋‹ฌ

const title = await modal.open(Prompt, {
  title: "๋ฌธ์„œ ์ œ๋ชฉ",
  placeholder: "์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”.",
});

if (title) {
  await saveTitle(title);
}

AbortController

import { MODAL_ABORTED } from "@reactleaf/modal";

const controller = new AbortController();
const timer = window.setTimeout(() => controller.abort(), 3000);

const result = await modal.open(Alert, { message: "3์ดˆ ํ›„ ์ž๋™์œผ๋กœ ๋‹ซํž™๋‹ˆ๋‹ค." }, { abortController: controller });

window.clearTimeout(timer);

if (result === MODAL_ABORTED) {
  console.log("๋ชจ๋‹ฌ์ด abort๋กœ ๋‹ซํ˜”์Šต๋‹ˆ๋‹ค.");
}

React ์ปดํฌ๋„ŒํŠธ ๋ฐ–์—์„œ ์—ด๊ธฐ

manager ์ธ์Šคํ„ด์Šค๋ฅผ importํ•  ์ˆ˜ ์žˆ๋Š” ๊ณณ์ด๋ผ๋ฉด React ์ปดํฌ๋„ŒํŠธ ๋ฐ–์—์„œ๋„ ๋ชจ๋‹ฌ์„ ์—ด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { modal } from "./modal";
import Alert from "./modals/Alert";

window.addEventListener("error", () => {
  void modal.open(Alert, { message: "์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." });
});

์• ๋‹ˆ๋ฉ”์ด์…˜

๋ชจ๋‹ฌ ๋ ˆ์ด์–ด๋Š” ๋งˆ์šดํŠธ ์งํ›„ ํ•œ ํ”„๋ ˆ์ž„ ๋’ค์— visible ์ƒํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๋ ˆ์ด์–ด์˜ dim fade๋Š” .modal-layer.visible๋กœ, ๋ชจ๋‹ฌ content์˜ ์—ด๋ฆผ/๋‹ซํž˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์€ .modal-layer[data-content-visible="true"]๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ซํž˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํ•„์š”ํ•˜๋ฉด closeDelay๋ฅผ CSS transition ์‹œ๊ฐ„๊ณผ ๋งž์ถฅ๋‹ˆ๋‹ค.

<ModalProvider manager={modal} defaultLayerOptions={{ closeDelay: 180 }}>
  <App />
</ModalProvider>
.modal-layer {
  opacity: 0;
  transition: opacity 180ms ease;
}

.modal-layer.visible {
  opacity: 1;
}

.modal-layer > * {
  transform: translateY(8px) scale(0.98);
  transition: transform 180ms ease;
}

.modal-layer[data-content-visible="true"] > * {
  transform: translateY(0) scale(1);
}

๋ชจ๋‹ฌ ๋‚ด๋ถ€ UI์—๋„ ๊ฐ™์€ ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const { visible } = useModalInstance();

๊ธฐ๋ณธ ๋™์ž‘

  • Escape ํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ๊ฐ€์žฅ ์œ„์˜ ๋ชจ๋‹ฌ์ด ๋‹ซํž™๋‹ˆ๋‹ค.
  • ๋ธŒ๋ผ์šฐ์ € ๋’ค๋กœ๊ฐ€๊ธฐ๋ฅผ ๋ˆ„๋ฅด๋ฉด ๊ฐ€์žฅ ์œ„์˜ ๋ชจ๋‹ฌ์ด ๋‹ซํž™๋‹ˆ๋‹ค.
  • closeOnOutsideClick์„ false๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์ตœ์ƒ์œ„ ๋ชจ๋‹ฌ์˜ ๋ฐ”๊นฅ ์˜์—ญ ํด๋ฆญ์œผ๋กœ ๋‹ซํž™๋‹ˆ๋‹ค.
  • ์—ฌ๋Ÿฌ ๋ชจ๋‹ฌ์€ ์—ด๋ฆฐ ์ˆœ์„œ๋Œ€๋กœ stack์— ์Œ“์ž…๋‹ˆ๋‹ค.
  • rootOptions.preventScroll์ด true์ด๋ฉด ๋ชจ๋‹ฌ์ด ์—ด๋ ค ์žˆ๋Š” ๋™์•ˆ body scroll์„ ๋ง‰์Šต๋‹ˆ๋‹ค.
  • dim์ด true์ด๋ฉด ํ•ด๋‹น ๋ชจ๋‹ฌ layer์— dim class๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • dim์— ๋ฌธ์ž์—ด์„ ์ „๋‹ฌํ•˜๋ฉด ํ•ด๋‹น ๋ฌธ์ž์—ด์„ ๋ชจ๋‹ฌ layer์˜ custom dim class๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

์Šคํƒ€์ผ๋ง

๊ธฐ๋ณธ ์Šคํƒ€์ผ์‹œํŠธ๋ฅผ importํ•ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import "@reactleaf/modal/style.css";

์ฃผ์š” selector:

  • .modal-layer
  • .modal-layer.visible
  • .modal-layer[data-content-visible="true"]
  • .modal-layer.dim
.modal-layer {
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-layer.dim {
  background: rgba(0, 0, 0, 0.6);
}

.modal-layer[data-content-visible="true"] > * {
  transform: translateY(0) scale(1);
}

๋ ˆ์ด์–ด ์ž์ฒด์— ์ปค์Šคํ…€ class๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด className ์˜ต์…˜์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

await modal.open(Confirm, { message: "์ง„ํ–‰ํ• ๊นŒ์š”?" }, { className: "danger-modal-layer" });

๊ธฐ๋ณธ dim class ๋Œ€์‹  ๋‹ค๋ฅธ class๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด dim์— ๋ฌธ์ž์—ด์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

await modal.open(Confirm, { message: "์ง„ํ–‰ํ• ๊นŒ์š”?" }, { dim: "danger-dim" });

Smooth Sequential Flow

replaceSelf(...)๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ˜„์žฌ ๋ชจ๋‹ฌ์˜ layer์™€ dim์„ ์œ ์ง€ํ•œ ์ฑ„ content๋งŒ ๋‹ค์Œ ๋ชจ๋‹ฌ๋กœ ๊ต์ฒดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ ์ž…๋ ฅ ํ›„ ์ธ์ฆ๋ฒˆํ˜ธ ์ž…๋ ฅ์œผ๋กœ ๋„˜์–ด๊ฐ€๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ํ•œ ํ๋ฆ„ ์•ˆ์—์„œ ๋‹จ๊ณ„๊ฐ€ ๋ฐ”๋€Œ๋Š” UI์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

import { type ModalComponent, useModalInstance } from "@reactleaf/modal";
import { modal } from "./modal";
import CodeModal from "./CodeModal";
import EmailModal from "./EmailModal";

type EmailModalProps = {
  onVerified: () => Promise<void>;
};

function startEmailVerification() {
  void modal.open(EmailModal, {
    onVerified: completeSignIn,
  });
}

const EmailModal: ModalComponent<EmailModalProps, never> = ({ onVerified }) => {
  const { replaceSelf } = useModalInstance();

  async function submitEmail(email: string) {
    await sendVerificationCode(email);

    const verified = await replaceSelf(CodeModal, {
      email,
    });

    if (verified) {
      await onVerified();
    }
  }
};

์ด์ „ ๋ชจ๋‹ฌ์˜ open() Promise๋Š” MODAL_REPLACED๋กœ resolve๋˜๊ณ , replaceSelf()๊ฐ€ ๋ฐ˜ํ™˜ํ•œ Promise๋Š” ์ƒˆ ๋ชจ๋‹ฌ์˜ ๊ฒฐ๊ณผ๋กœ resolve๋ฉ๋‹ˆ๋‹ค. replace ์ค‘์—๋Š” layer๊ฐ€ ์œ ์ง€๋˜๋ฏ€๋กœ dim์ด ์‚ฌ๋ผ์กŒ๋‹ค๊ฐ€ ๋‹ค์‹œ ๋‚˜ํƒ€๋‚˜์ง€ ์•Š๊ณ , content๋งŒ ๋‹ซํžŒ ๋’ค ์ƒˆ content๊ฐ€ ์—ด๋ฆฝ๋‹ˆ๋‹ค.

๋™์ž‘ ์˜ˆ์ œ

GitHub Pages์—์„œ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์˜ˆ์ œ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

https://reactleaf.github.io/modal/

๋กœ์ปฌ์—์„œ ์‹คํ–‰ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

pnpm install
pnpm dev:docs