npm package: @react-crates/modal
Description: 리액트 모달을 쉽게 등록하고 사용가능한 라이브러리입니다.
Author: wjunhee102
Latest version: 0.7.2

# @react-crates/modal

![react-crates-modal-demo](https://github.com/wjunhee102/react-crates/assets/59816563/46c6fe41-7daa-45ba-8a4f-fd079894c0aa)

**@react-crates/modal**은 React 프로젝트에서 기본 JavaScript modal을 효과적으로 대체하고, 사용자의 요구에 맞춰 모달의 위치, 애니메이션, 및 실행 결과를 맞춤 설정할 수 있는 강력한 라이브러리입니다. <br />
[@react-crates/modal 문서](https://wjunhee102.github.io/react-crates/)로 쉽게 시작해보세요!

## Installation

```
$ npm install --save @react-crates/modal
$ yarn add @react-crates/modal
$ pnpm add @react-crates/modal
```

## Table of Contents

- [Features](#features)
- [The gist](#the-gist)
- [Demo](#demo)
- [API](#api)
- [Documentation](#documentation)
- [License](#license)

## Features

- **효율적 개발**: 본 라이브러리는 모달의 간편한 생성과 재사용을 통해 개발 효율성을 극대화합니다.
- **기본 modal 대체**: window.alert와 window.confirm 같은 기본 JavaScript modal을 React 프로젝트 내에서 효과적으로 대체할 수 있습니다.
- **사전 구성된 컴포넌트**: 미리 정의된 컴포넌트들을 제공하여, 개발자가 손쉽고 빠르게 모달을 사용할 수 있도록 합니다.
- **유연한 사용자 정의**: 실행 결과를 사용자가 원하는 대로 맞춤 설정할 수 있으며, 애니메이션 통합과 화면 크기에 따른 위치 조정이 가능합니다.
- **React 컨텍스트 내 독립 실행**: 본 라이브러리는 React 애플리케이션 내에서 컴포넌트의 상위 계층과 독립적으로 모달을 생성하고 관리할 수 있는 기능을 제공합니다. 이를 통해 React-toastify와 유사하게, 애플리케이션의 어느 곳에서나 간편하게 모달을 호출하고 활용할 수 있습니다.
- **웹 접근성(Web Accessibility)**: 웹 접근성 기준을 준수하여 개발되었습니다.
- **제로 의존성(zero dependencies)**: 제로 의존성으로 인해 보안 리스크를 최소화하고, 프로젝트의 복잡성 없이 안정적으로 통합할 수 있습니다.

## The gist

```javascript
import React from "react";

import { generateModal, modalCollection } from "@react-crates/modal";

const { modalCtrl, ModalProvider } = generateModal({
  ...modalCollection,
});

function App() {
  const alert = () => modalCtrl.alert("알림");

  return (
    <div>
      <button onClick={alert}>알림</button>
      <ModalProvider />
    </div>
  );
}
```

## Demo

설명서에 나와있는 내용을 [데모](https://codesandbox.io/p/sandbox/junhee-h-react-modal-cd4j7m?file=%2Fsrc%2FApp.tsx)에서 확인해보세요.

## API

### generateModal

```tsx
// modal.ts
import { generateModal } from "@react-crates/modal";

const {
  modalCtrl, // modal을 실행시키기 위한 ctrl입니다.
  ModalProvider, // modal component가 렌더되는 곳입니다.
  DynamicModal, // React Component내에 사용가능한 Modal입니다.
  useInOpenModal // modal이 open 유무를 확인할 수 있는 hook입니다.
} = generateModal({
  [Name: string]: {
    component: ModalFC<T = any, string = string>;
    /* modal의 기본 설정을 등록할 수 있습니다. */
    defaultOptions?: {
      payload?: T; // modal과 통신이 필요할 때 사용할 수 있습니다.
      modalKey?: string; // 동일한 modal이 동시에 실행되지 않게 할 수 있습니다.
      /* action: modal의 action을 실행시켰을때 동작할 callback 입니다. */
      action?: (confirm?: boolean | string) => void | Promise<void>;
      /* ModalMiddleware: action callback을 인터셉터하여 원하는 로직을 수행하게 할 수 있습니다. */
      middleware?: ModalMiddleware;
      backCoverConfirm?: boolean | string | null //null 일 경우 동작하지 않습니다.
      backCoverColor?: string;
      backCoverOpacity?: number;
      escKeyActive?: boolean; //  esc 버튼으로 cancel action을 실행시킬 수 있습니다.
      closeDelay?: number; // modal이 설정한 delay후 close 됩니다.
      duration?: number; // modal이 생성, 닫힐 때 실행되는 transition의 속도입니다.
      transitionOptions?: { // modal이 생성, 닫힐 때 실행되는 transition의 옵션입니다.
        transitionProperty?: string;
        transitionTimingFunction?: string;
        transitionDelay?: string;
      };
      /* modal의 위치를 설정할 수 있습니다. */
      position?: string | ((breakPoint: number) => string);
      /* modalActionState에 따라 자동으로 Modal Componet가 변경됩니다. */
      stateResponsiveComponent?: boolean;
      /* modal이 focus 될 때 동작하는 로직을 작성할 수 있습니다. */
      onOpenAutoFocus?: FocusEventHandler<HTMLDivElement>;
      /* 접근성 관련 property입니다. */
      label?: string;
      role?: string;
      /* 동적으로 modal의 내용을 입력할 수 있습니다. */
      title?: React.ReactNode;
      subTitle?: React.ReactNode;
      content?: React.ReactNode;
      subContent?: React.ReactNode;
      confirmContent?: React.ReactNode;
      cancelContent?: React.ReactNode;
      customActionContent?: React.ReactNode;
      /* 등록된 modal이 덮혀씌워지지 않거나 지워지지 않습니다. */
      required?: boolean;
    }
  }
},
/* 등록한 모달 전체의 default 값을 설정할 수 있습니다. */
{
  stateResponsiveComponent?: boolean; // modal이 actionState에 따라 자동으로 변경
  backCoverColor?: string;
  backCoverOpacity?: number;
  duration?: number;
  transition?: {
    transitionProperty?: string;
    transitionTimingFunction?: string;
    transitionDelay?: string;
  };
  /* modal의 position을 등록할 수 있습니다. */
  position?: {
    [Name: string]: {
      [open | active | close]: {
        left?: string;
        right?: string;
        top?: string;
        bottom?: string;
        transform?: string;
        opacity?: number;
        background?: string;
        className?: string;
      }
    }
  };
});
```

### modalCtrl

```tsx
import { modalCtrl } from "./modal.ts";

/* modalCtrl.open의 첫번째 인자 */

// modal name
modalCtrl.open("alert");

// 동적 modal 생성
modalCtrl.open(() => <div>alert</div>);

/* modalCtrl.open의 두번째 인자 */

// modal action
modalCtrl.open("alert", (confirm?: boolean | string) => {
  ... // confirm에 따라 수행할 로직을 작성하시면 됩니다.
});

// modal dispatch options
modalCtrl.open("alert", {
  payload?: T; // modal과 통신이 필요할 때 사용할 수 있습니다.
  modalKey?: string; // 동일한 modal이 동시에 실행되지 않게 할 수 있습니다.
  // modal의 action을 실행시켰을때 동작할 callback 입니다.
  action?: (confirm?: boolean | string) => void | Promise<void>;
  // action callback을 인터셉터하여 원하는 로직을 수행하게 할 수 있습니다.
  backCoverConfirm?: boolean | string | null //null 일 경우 동작하지 않습니다.
  backCoverColor?: string;
  backCoverOpacity?: number;
  escKeyActive?: boolean; // esc 버튼으로 cancel action을 실행시킬 수 있습니다.
  closeDelay?: number; // modal이 설정한 delay후 close 됩니다.
  duration?: number; // modal이 생성, 닫힐 때 실행되는 애니메이션 속도입니다.
  transitionOptions?: {
    transitionProperty?: string;
    transitionTimingFunction?: string;
    transitionDelay?: string;
  };
  // modal의 위치를 설정할 수 있습니다.
  position?: string | ((breakPoint: number) => string);
  // modalActionState에 따라 자동으로 Modal Componet가 변경됩니다.
  stateResponsiveComponent?: boolean;
  // 동적으로 modal의 내용을 입력할 수 있습니다.
  title?: React.ReactNode;
  subTitle?: React.ReactNode;
  content?: React.ReactNode;
  subContent?: React.ReactNode;
  confirmContent?: React.ReactNode;
  cancelContent?: React.ReactNode;
  customActionContent?: React.ReactNode;
});

```

## Documentation

### 설치

```markdown
$ npm install --save @react-crates/modal
$ yarn add @react-crates/modal
$ pnpm add @react-crates/modal
```

### 초기 설정

- modal.ts 파일에 `generateModal`에 `modal을 등록하고 초기 option값을 설정`합니다.

```tsx
// modal.ts
import { generateModal } from "@react-crates/modal";

export const { modalCtrl, ModalProvider, DynamicModal, useInOpenModal } =
  generateModal(...modalComponents, ...defaultOptions);

// App.tsx
import { ModalProvider } from "./modal";

function App() {
  return (
    <div>
      ...
      <ModalProvider />
    </div>
  );
}
```

### Modal Component 만들기

```tsx
// ExampleModal.tsx
import { ModalFC } from "@react-crates/modal";

const ExampleModal: ModalFC = ({
  title,
  content,
  confirmContent,
  cancelContent,
  action
}) => {
  return (
    <div>
      <h2>{title || "타이틀"}</h2>
      <p>{content || "내용"}</p>
      <button onClick={() => action(false)}>
        {confirmContent || "취소"}
      </button>
      <button onClick={() => action(true)}>
        {cancelContent || "확인"}
      </button>
    </div>
  );
}

// ModalComponentProps은 아래와 같이 구성되어 있습니다.

interface ModalComponentProps<T> {
  title?: ReactNode;
  subTitle?: ReactNode;
  content?: ReactNode;
  subContent?: ReactNode;
  confirmContent?: ReactNode;
  cancelContent?: ReactNode;
  customActionContent?: ReactNode;
  // modal의 action 상태
  actionState: "initial" | "pending" | "success" | "error";
  action: (confirm?: string | boolean) => void; // modal 실행
  payload?: T; // modal 통신이 필요하면 해당 프로퍼티를 활용해보세요.
}

//useModalComponentProps를 이용한 ModalComponent 만들기
import { useModalComponentProps } from "@react-crates/modal";

const ExampleModal = () => {

  const {
    title,
    content,
    confirmContent,
    cancelContent,
    action
  } = useModalComponentProps();

  return (
   <div>
      <h2>{title || "타이틀"}</h2>
      <p>{content || "내용"}</p>
      <button onClick={() => action(false)}>
        {confirmContent || "취소"}
      </button>
      <button onClick={() => action(true)}>
        {cancelContent || "확인"}
      </button>
    </div>
  );
}


// Modal을 이용한 ModalComponent 만들기

import { Modal } from "@react-crates/modal";

const ExampleModal = () => {
  return (
    <div>
      {/* modal을 open할 때 children으로 입력한 값을 대치합니다. */}
      <Modal.Title>타이틀</Modal.Title>
      <Modal.Content>내용</Modal.Content>
      <Modal.Action.Cancel>취소</Modal.Action.Cancel>
      <Modal.Action.Confirm>확인</Modal.Action.Confirm>
    </div>
  );
}

// Modal Util Component 목록

<Modal.Title />
<Modal.Title.Sub />
<Modal.Content />
<Modal.Content.Sub />
<Modal.Action />
<Modal.Action.Confirm />
<Modal.Action.Cancel />
<Modal.Action.Custom />
```

### Modal Template 이용

- ModalTemplate은 style이 적용 되어있는 preset Component입니다.
- ModalTemplate을 활용하여 쉽게 Modal의 레이아웃을 쉽게 구성할 수 있습니다.

```tsx
import { ModalTemplate, Modal } from "@react-crates/modal";

const ExampleModal = () => {
  return (
    <ModalTemplate>
      <ModalTemplate.Header>
        <Modal.Title>타이틀</Modal.Title>
      </ModalTemplate.Header>

      <ModalTemplate.Main>
        <Modal.Content>내용</Modal.Content>
      </ModalTemplate.Main>

      <ModalTemplate.Footer>
        <Modal.Action.Cancel>취소</Modal.Action.Cancel>
        <Modal.Action.Confirm>확인</Modal.Action.Confirm>
      </ModalTemplate.Footer>
    </ModalTemplate>
  );
};

// Modal Template 목록
<ModalTemplate>
<ModalTemplate.Header>
<ModalTemplate.Main>
<ModalTemplate.Footer>
```

### Modal 등록

```tsx
// modal.ts
import { generateModal } from "@react-crates/modal";
import ExampleModal from "./ExampleModal";

export const { modalCtrl } = generateModal({
	// 이름은 일부 예약어를 제외하고는 자유롭게 적으시면 됩니다.
  confirm: {
    component: ExampleModal,
    defaultOptions: {
      ...
    }
  }
}, {
	...
});

// reserved modal name
// 해당 이름으로 모달을 만들경우 무시됩니다.
type ReservedModalName =
  | "clear"
  | "unknown"
  | "open"
  | "close"
  | "edit"
  | "remove"
  | "action"
  ;

```

### Modal Collection 이용

- Modal Collection은 style이 적용 되어있는 preset Modal Component입니다.
- Modal Collection을 이용하면 별도의 Modal Component 개발 없이도 바로 사용할 수 있습니다.
- defaultOptions을 통해 제목, 내용, 버튼 등의 콘텐츠를 개별적으로 설정할 수 있습니다.

```tsx
// modal.ts
import { generateModal, modalCollection } from "@react-crates/modal";

const {
  confirm,
  alert,
  prompt
} = modalCollection

export const { modalCtrl } = generateModal({
  confirm,
  prompt,
  alert: {
    component: alert.component,
    defaultOptions: {
      ...alert.defaultOptions,
      title: "알림",
      content: "알림",
      confirmContent: "확인",
    },
  },
});

// Modal Collection Confirm의 구성
<ModalTemplate>
  <ModalTemplate.Header>
    <Modal.Title className="modal-collection-title-rm">Confirm</Modal.Title>
    <Modal.Title.Sub className="modal-collection-sub-title-rm" />
  </ModalTemplate.Header>

  <ModalTemplate.Main>
    <Modal.Content className="modal-collection-content-rm" />
    <Modal.Content.Sub className="modal-collection-sub-content-rm" />
  </ModalTemplate.Main>

  <ModalTemplate.Footer>
    <Modal.Action.Cancel className="modal-collection-action-rm modal-collection-cancel-rm">
      Cancel
    </Modal.Action.Cancel>
    <Modal.Action.Confirm className="modal-collection-action-rm modal-collection-confirm-rm">
      Confirm
    </Modal.Action.Confirm>
  </ModalTemplate.Footer>
</ModalTemplate>

// Modal Collection 목록
{
  confirm: {
    component: Confirm,
    defaultOptions: {
      backCoverConfirm: false,
      escKeyActive: true,
      role: "dialog",
      label: "confirm",
    },
  },
  alert: {
    component: Alert,
    defaultOptions: {
      backCoverConfirm: true,
      escKeyActive: true,
      role: "alertdialog",
      label: "alert",
    },
  },
  prompt: {
    component: Prompt,
    defaultOptions: {
      backCoverConfirm: undefined,
      escKeyActive: true,
      role: "dialog",
      label: "prompt",
    },
  },
};
```

### Modal 사용

```tsx
import { modalCtrl } from "./modal";

function Example() {
  const confirm = () => {

    // 등록한 모달의 이름을 입력합니다.
    modalCtrl.open("confirm");

    // modal을 등록하면 controller 메소드로 형성됩니다.
    modalCtrl.confirm();

    // modal의 버튼을 클릭했을 시 실행될 함수를 입력할 수 있습니다.
    modalCtrl.confirm((confirm?: boolean | string) => {
      if (confirm === true) {
        ... // 확인 버튼을 클릭했을 때
      } else if (confirm === false {
        ... // 취소, back cover를 클릭했을 때
      } else if (confirm === string /* 사용자 정의 */) {
        ... // 확인 취소 이외에 action이 필요할 경우
      }
    });

    // modal의 action에서 비동기 함수를 실행 시킬 수 있습니다.
    modalCtrl.confirm(async (confirm?: boolean | string) => {
      if (confirm === true) {
        const result = await someAsyncCallback();

        console.log(result);
      } else {
        console.log("취소");
      }
    }
  }

  return (
    <div>
      <button onClick={alert}>alert</button>
    </div>
  );
}
```

- `Modal의 콘텐츠`를 동적으로 설정할 수 있습니다.

```tsx
function Example() {
  const confirm = () => {
    modalCtrl.open({
      title: "확인해주세요.",
      content: "확인하시겠습니까?",
      cancelContent: "취소 버튼",
      confirmContent: "확인 버튼",
    });
  };

  return (
    <div>
      <button onClick={alert}>alert</button>
    </div>
  );
}

/*
  <div>
    <h2>알림 제목</h2> <- Modal.Title
    <p>알림입니다.</p> <- Modal.Content
    <button>취소 버튼</button> <- Modal.Action.Cancel
    <button>확인 버튼</button> <- Modal.Action.Confirm
  </div>
*/
```

### Modal Collection Prompt 사용자 입력값 가져오기

```tsx
import { modalCtrl } from "./modal";

function Example() {
  const prompt = () => {
    modalCtrl.prompt();

    modalCtrl.prompt((confirm) => {
      console.log(confirm /* 사용자 입력값 확인 가능 */);
    });
  };

  return (
    <div>
      <button onClick={prompt}>prompt</button>
    </div>
  );
}

// ModalCollection.Prompt Component
const Prompt = () => {
  const [state, setState] = useState("");
  const { action } = useModalComponentProps();

  const { actionToKeyUp, onChange } = useMemo(
    () => ({
      actionToKeyUp(event: KeyboardEvent<HTMLInputElement>) {
        if (event.key !== "Enter") {
          return;
        }

        event.preventDefault();
        action(state);
      },
      onChange(event: ChangeEvent<HTMLInputElement>) {
        setState(event.target.value);
      },
    }),
    []
  );

  return (
    <ModalTemplate className="modal-template-bg-rm">
      <ModalTemplate.Header>
        <Modal.Title className="modal-collection-title-rm">Prompt</Modal.Title>
        <Modal.Title.Sub className="modal-collection-sub-title-rm" />
      </ModalTemplate.Header>

      <ModalTemplate.Main>
        <Modal.Content className="modal-collection-content-rm" />
        <Modal.Content.Sub className="modal-collection-sub-content-rm" />
        <div className="modal-collection-prompt-rm">
          <input
            onChange={onChange}
            onKeyUp={actionToKeyUp}
            className="modal-collection-prompt-input-rm"
          />
        </div>
      </ModalTemplate.Main>

      <ModalTemplate.Footer>
        <Modal.Action.Cancel className="modal-collection-action-rm modal-collection-cancel-rm">
          Cancel
        </Modal.Action.Cancel>
        <Modal.Action.Custom
          className="modal-collection-action-rm modal-collection-confirm-rm"
          confirmType={state}
        >
          Confirm
        </Modal.Action.Custom>
      </ModalTemplate.Footer>
    </ModalTemplate>
  );
};
```

### Modal 동적 생성

- `Modal을 동적`으로 생성할 수 있습니다.

```tsx
import { modalCtrl } from "./modal";

function Example() {
  const confirm = () => {
    modalCtrl.open(() => (
      <div>
        <Modal.Title>타이틀</Modal.Title>
        <Modal.Content>내용</Modal.Content>
        <Modal.Action.Cancel>취소</Modal.Action.Cancel>
        <Modal.Action.Confirm>확인</Modal.Action.Confirm>
      </div>
    ));
  };

  return (
    <div>
      <button onClick={confirm}>alert</button>
    </div>
  );
}
```

### Modal Positioning

- 유동적 위치 지정: `position` 속성을 사용하여 modal의 위치를 동적으로 조정할 수 있습니다.
- 복합 위치 설정: 여러 `position` 값들을 조합하여 modal의 생성 및 소멸 위치를 세밀하게 설정할 수 있습니다.
- 조건부 위치 설정: `breakPoint(width)` 값을 기반으로 화면 크기에 따라 modal의 위치를 조정할 수 있습니다. 이를 통해 반응형 디자인에 적합하게 모달을 위치시킬 수 있습니다.

```tsx
import { modalCtrl } from "./modal";

function Example() {
  const confirm = modalCtrl.confirm({
    position: "center"; // center에서 생성되고 사라집니다.
  });

  const confirm = modalCtrl.confirm({
    position: "bottom-center"; // bottom에서 시작해서 center에 생성되고 사라집니다.
  });

  const confirm = modalCtrl.confirm({
    // bottom에서 시작해서 center에 생성되고 bottom에서 사라집니다.
    position: "bottom-center-bottom";
  });

  const confirm = modalCtrl.confirm({
    //
    position: (breakPoint) => breakPoint > 425 ? "center" : "bottom";
  });
}
```

- 본인만의 `position`을 만들고 조합해서 사용할 수 있습니다.

```tsx
// modal.ts
import { generateModal } from "@react-crates/modal";

export const { modalCtrl } = generatorModal({
  ...modalComponentTable
}, {
  position: {
    // 사용자에 맞춘 커스텀 position을 만들 수 있습니다.
    customPosition: {
      open: {
        left: "0px",
        right: "0px",
        ...
      },
      active: {
        ...
      },
      close: {
        ...
      }
    }
  }
});

---
// Example.tsx
import { modalCtrl } from "./modal";

function Example() {
  const confirm = modalCtrl.confirm({
    // 순서대로 open, active, close의 설정한 style이 적용됩니다.
    // ex) customPosition이 처음에 있으면 customPosition의 open style.
    // ex) customPosition이 중간에 있으면 customPosition의 active style이 적용.
    position: "center-customPosition-top";
  });
}

// Default Position 목록입니다.

type DefaultModalPosition =
  | "default"
  | "bottom"
  | "top"
  | "left"
  | "right"
  | "center"
  | "leftTop"
  | "leftBottom"
  | "rightTop"
  | "rightBottom";
```

### Modal Payload

- Modal과 통신이 필요한 경우 `payload` option을 사용할 수 있습니다.
- payload는 항상 `undefined`일 수 있습니다.
- payload의 타입을 `defaultOptions`에 적용하면 `modalCtrl`에서 `IntelliSense`가 활성화 됩니다.

```tsx
import { ModalFC } from "@react-crates/modal";

export interface ExamplePayload {
  foo: string;
  bar: number;
}

export const ExampleModal: ModalFC<ExamplePayload> = ({
  payload /* type: ExamplePayload | undefined */
}) => {
  return (
    <div>
      ...
    </div>
  );
}

--
// modal.ts
import { ExampleModal, ExamplePayload } from "./ExampleModal";

const { modalCtrl } = generateModal({
  confirm: {
    component: ExampleModal,
    defaultOptions: {
      // case 1
      payload: {
        foo: "foo",
        bar: 1,
      }
      // case 2
      payload: undefined as ExamplePayload | undefined
    }
  }
});

//
modalCtrl.confirm({
  payload: {
    ... // IntelliSense 활성화
  }
});
```

### Modal Active State

- `Modal action` 실행 중에 상태를 변경하여 `Modal의 Component`를 변경할 수 있습니다.
- `비동기 action`을 위해 만들졌습니다.
- 기본 action 상태는 `“initial"`입니다.

```tsx
import { useState } from "react";
import { modalCtrl } from "./modal";

function Example() {
  const [message, setMessage] = useState(null);

  const confirm = () => {

    modalCtrl.confirm(async (confirm, { pending, success, error }) => {
      // 기본 actionState는 "initial"입니다.
      if (!confirm) {
        return;
      }

      pending(); // actionState를 "pending"으로 변경합니다.

      try {
        const data = await api();

        setMessage(data);
        success(); // actionState를 "success"으로 변경합니다.
      } catch (someError) {
        error(); // actionState를 "error"으로 변경합니다.
      }
    });

  }

  return (
    <div>
      <button onClick={confirm}>confirm</button>
    </div>
  );
}

---
// Modal Action State 목록입니다.

type ModalActionState =
  | "initial"
  | "pending"
  | "success"
  | "error"
```

### Modal Action State 활용

```tsx
// modal.ts
import { generateModal } from "@react-crates/modal";

const { modalCtrl } = generateModal({
  confirm: {
    component: () => <div></div>,
    // 해당 옵션을 활성해야 자동 state response 기능이 활성화됩니다.
    stateResponsiveComponent: true
  },
  // 아래에 name으로 modal을 등록하면 actionState에 활용할 수 있습니다.
  pending: {
    ...
  },
  success: {
    ...
  },
  error: {
    ...
  }

}, {
  ...
  stateResponsiveComponent: true // 전체 모달의 기본 값으로 설정할 수 있습니다.
});

modalCtrl.confirm(async (confirm, {
  initial,
  pending,
  success,
  error,
  end
}) => {

  initial() // modal이 기존 modal component를 유지합니다.
  pending() // modal이 등록된 "pending" modal로 변경됩니다.
  success() // modal이 등록된 "success" modal로 변경됩니다.
  error() // modal이 등록된 "error" modal로 변경됩니다.
  end() // modal이 기존 modal component로 돌아갑니다.

  pending("로딩 중..."); // modal content가 변경됩니다.

});

// 다른 등록된 modal로 변경할 수 있습니다.
modalCtrl.confirm(async (confirm, { pending }) => {

  // 등록된 modal 확용
  pending({
    component: "example",
  });

  // 동적 modal component 생성
  pending({
    component: () => <div>로딩...</div>,
  });

});

// Modal Component에 직접 actionState를 활용할 수 있습니다.
import { ModalFC } from "@react-crates/modal";

const ExampleModal: ModalFC = ({ actionState }) => {
  if (actionState === "initial") {
    return (
      <div>initial</div>
    );
  }

  if (actionState === "pending") {
    return (
      <div>loading...</div>
    );
  }

  ...
}
```

### AfterCloseCallback

- `modal이 close`되고 나서 실행되는 함수입니다.
- `AfterCloseCallback`은 `success`, `error`, `end` 메소드만 등록할 수 있습니다.

```tsx
modalCtrl.confirm(async (confirm, { success, error, end }) => {

  // 기본 사용법
  success(() => {
    window.open("test.com");
  });

  // 더 많은 옵션을 추가하는 법.
  success({
    afterCloseCallback: () => {
      window.open("test.com");
    },
    // 옵션을 활성화 시키면 이 action을 modal close 되지 않습니다.
    // 다시 action을 눌렀을 때는 지정한 action을 수행하지 않고 바로 종료됩니다.
    // 사용자에게 결과를 확인 시킨 후 종료할 수 있는 기능입니다.
    isAwaitingConfirm?: boolean,
    component?: string | ModalComponent // 다른 component로 변경
    options?: {
      title?: ReactNode;
      subTitle?: ReactNode;
      content?: ReactNode;
      subContent?: ReactNode;
      confirmContent?: ReactNode;
      cancelContent?: ReactNode;
      customContent?: ReactNode;
    }
  });

});
```

### DynamicModal

- `DynamicModal`은 `React 컴포넌트`의 `자연스러운 흐름에 따라 구현`할 수 있는 모달입니다.
- 기존의 모달 개발 방식을 활용하여 직관적으로 모달을 구성하고 관리할 수 있습니다.
- `props`를 통해 기존 modal처럼 설정할 수 있습니다.

```tsx
import { generateModal } from "@react-crates/modal";

export const { DynamicModal } = generateModal();

function Example() {
  return (
    <div>
      <DynamicModal
        duration={250}
        position="center"
        action={(confirm?: boolean | string) => {
          ...
          return;
        }}
      >
        {/* trigger는 모달을 open하는 버튼입니다. */}
        <DynamicModal.Trigger>confirm</DynamicModal.Trigger>

        {/* element 내부의 component가 modal로 출력됩니다. */}
        <DynamicModal.Element>
          <div>
            <h2>타이틀</h2>
            <p>내용</p>
            <DynamicModal.Action.Cancel>취소</DynamicModal.Action.Cancel>
            <DynamicModal.Action.Confirm>확인</DynamicModal.Action.Confirm>
          </div>
        </DynamicModal.Element>
      </DynamicModal>
    </div>
  );
}

/** DynamicModal Props **/
interface DynamicModalProps {
  action?: ModalCallback;
  backCoverConfirm?: string | boolean | null;
  backCoverColor?: string;
  backCoverOpacity?: number;
  escKeyActive?: boolean;
  closeDelay?: number;
  duration?: number;
  transitionOptions?: {
    transitionProperty: string;
    transitionTimingFunction: string;
    transitionDelay: string;
  };
  position?: string;
  stateResponsiveComponent?: boolean;
  onOpenAutoFocus?: FocusEventHandler<HTMLDivElement>;
}
```

### Modal Middleware

- Modal의 action을 가로채어 원하는 로직을 구현할 수 있습니다.
- 다음의 코드는 default로 동작하는 middleware입니다.

```tsx
async function defaultMiddleware({ modalState }: ModalMiddlewareProps) {
  if (modalState.isAwaitingConfirm) {
    return modalState.close();
  }

  await modalState.callback(modalState.confirm, modalState);

  if (modalState.isCloseDelay) {
    await delay(modalState.closeDelayDuration);

    return modalState.close();
  }

  if (modalState.isAwaitingConfirm) {
    return false;
  }

  return modalState.close();
}

// types

interface ModalMiddlewareProps {
  modalState: Modal;
}

class Modal {
  get id(): number;
  get options(): ModalOptions<any, string>;
  get modalKey(): string | null;
  get name(): string;
  get component(): ModalComponent;
  get confirm(): ModalConfirmType | undefined;
  get isAwaitingConfirm(): boolean;
  get isCloseDelay(): boolean;
  get closeDelayDuration(): number;
  get callback(): ModalCallback;

  getActionState(): ModalActionState;
  getLifecycleState(): ModalLifecycleState;
  active(): void;
  close(): Promise<boolean>;
  init(): Promise<void>;
  blockCloseDelay(): this;
  setCloseDelay(duration: number): this;
  getState(): ModalState;
  subscribe(listener: (state: ModalState) => void): this;
  unsubscribe(listener: (state: ModalState) => void): this;
  notify(): this;
  getMiddlewareProps(): ModalMiddlewareProps;
  action(
    confirm?: ModalConfirmType,
    callback?: ModalCallback
  ): Promise<boolean>;
  initial(): this;
  pending(
    message?:
      | string
      | Omit<StateControllerOptions, "afterCloseCallback" | "isAwaitingConfirm">
  ): this;
  success(
    message?:
      | string
      | StateControllerOptions
      | ((confirm?: ModalConfirmType) => void)
  ): this;
  error(
    message?:
      | string
      | StateControllerOptions
      | ((confirm?: ModalConfirmType) => void)
  ): this;
  end(
    message?:
      | string
      | StateControllerOptions
      | ((confirm?: ModalConfirmType) => void)
  ): this;
  getModalStyle(): CSSProperties;
  getBackCoverStyle(): CSSProperties;
  setBreakPoint(breakPoint: number): void;
}
```

## License

Licensed under MIT