npm package: @sentuh/react-bootstrap-modal
Description: Lightweight and customizable React modal component styled with Bootstrap 5.
Author: Titus Sutio Fanpula
Latest version: 1.0.4

# @sentuh/react-bootstrap-modal

Lightweight React modal component built on Bootstrap 5, easy to use, supporting:

- `size`, `centered`, `backdrop`, `scrollable`, `fullscreen`
- Custom `modalClassName`
- ModalProvider for centralized state management
- `useModal` and `useActiveModal` hooks for flexible modal control

## Requirements & Installation

This library is a collection of React components built on top of Bootstrap 5. To ensure stability and compatibility, please ensure your project meets the following requirements.

### Peer Dependencies

These packages must be installed in your project. The library expects them to be available to avoid version conflicts and bundle bloat.

| Package   | Version    |
| --------- | ---------- |
| React     | `>=16.8.0` |
| React DOM | `>=16.8.0` |
| Bootstrap | `^5.0.0`   |

#### 1. Installation

Install the library via npm (or your preferred package manager):

```bash
npm install @sentuh/react-bootstrap-modal
```

#### 2. Setup Bootstrap

Since this library relies on Bootstrap styles, you must ensure the CSS is imported in your application's entry point (e.g., `src/main.tsx` or `src/App.js`):

```css
import "bootstrap/dist/css/bootstrap.min.css";
```

## Demo (live preview)

You can try a **live demo** with example code here: https://stackblitz.com/edit/react-bootstrap-modal

## Usage Overview

#### 1. Wrap Your App with ModalProvider

The `ModalProvider` manages all modal state in your app. Wrap it around your root component:

```typescript
import { ModalProvider } from "@sentuh/react-bootstrap-modal";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ModalProvider>
      <App />
    </ModalProvider>
  </StrictMode>
);
```

#### 2. Opening a Modal

There are two main ways to open a modal:

1. Passing the component class/function (recommended for reusability and automatic props handling)
2. Passing a JSX element directly (for quick one-off modals)

##### Method 1: Passing a Component (Recommended)

```typescript
import { useModal } from "@sentuh/react-bootstrap-modal";
import { ExampleModal } from "./ExampleModal";

function App() {
  const modal = useModal();

  const handleOpen = () => {
    // Pass the component itself, modal will render it internally
    const result = await modal.open(ExampleModal);
    console.log(result);
  };

  return (
    <button className="btn btn-primary" onClick={handleOpen}>
      Open Modal
    </button>
  );
}
```

ExampleModal Example:

```typescript
import type { ModalProps } from "@sentuh/react-bootstrap-modal";

interface Props extends ModalProps {}

export const ExampleModal = ({ onClose, onDismiss }: Props) => {
  return (
    <>
      <div className="modal-header">
        <h4>Example Modal Header</h4>
      </div>
      <div className="modal-body">
        <p>This is an example reusable modal component.</p>
      </div>
      <div className="modal-footer">
        <button className="btn btn-secondary" onClick={onDismiss}>
          Cancel
        </button>
        <button className="btn btn-primary" onClick={onClose}>
          Save
        </button>
      </div>
    </>
  );
};
```

**Note**: In this approach, `useModal.open()` will internally create and manage the modal instance.

You can pass dynamic data to your modal using the `model` argument:

```typescript
const model = { id: 1, name: "Jane Doe" };
const result = await modal.open(ExampleModal, model);
console.log(result); // { name: 'John Doe'}
```

Inside `ExampleModal`, access the passed data via the `model` prop:

```typescript
import type { ModalProps } from "@sentuh/react-bootstrap-modal";
import { useEffect } from "react";

interface InputProps {
  id: string;
  name: string;
}

interface OutputProps {
  name: string;
}

interface Props extends ModalProps<InputProps, OutputProps> {}

export const ExampleModal = ({ onClose, onDismiss, model }: Props) => {
  useEffect(() => {
    console.log(model); // input: { id: 1, name: 'Jane Doe'}
  }, []);

  const handleSave = () => {
    // return output to parent
    onClose({ name: "John Doe" });
  };

  return (
    <>
      <div className="modal-header">
        <h4>Example Modal Header</h4>
      </div>
      <div className="modal-body">
        <p>This is an example reusable modal component.</p>
      </div>
      <div className="modal-footer">
        <button className="btn btn-secondary" onClick={onDismiss}>
          Cancel
        </button>
        <button className="btn btn-primary" onClick={handleSave}>
          Save
        </button>
      </div>
    </>
  );
};
```

##### Method 2 — Passing JSX Element Directly

```typescript
import { useModal } from "@sentuh/react-bootstrap-modal";
import { ExampleModal } from "./ExampleModal";

function App() {
  const modal = useModal();

  const handleChange = (result: { name: string }) => {
    console.log(result); // output: { name: 'John Doe' }
  }

  const handleOpen = () => {
    // Pass the JSX element directly
    modal.open(<ExampleModal onChange={handleChange}/>);
  };

  return (
    <button className="btn btn-primary" onClick={handleOpen}>
      Open Modal
    </button>
  );
}
```

**Difference from Method 1**: In this approach, you pass a JSX element instead of the component reference. This is convenient for quick, one-off modals with inline props, but it is less reusable.

⚠️ **Important**: When using this approach, you need to implement useActiveModal inside your modal component to manually control closing or other modal actions. See Point 3 – Advanced Usage with useActiveModal for implementation examples.

#### 3. Advanced Usage with useActiveModal

`useActiveModal` allows the modal itself to control its lifecycle, such as closing itself.

```typescript
import { useActiveModal } from "@sentuh/react-bootstrap-modal";

interface Props {
  onChange: (result: { name: string }) => void;
}

export const ExampleModal = ({ onChange }: Props) => {
  const activeModal = useActiveModal();

  const handleSave = () => {
    activeModal.dismiss();
    onChange({ name: "John Doe" });
  };

  return (
    <>
      <div className="modal-header">
        <h4>Example Modal Header</h4>
      </div>
      <div className="modal-body">
        <p>This is an example reusable modal component.</p>
      </div>
      <div className="modal-footer">
        <button className="btn btn-secondary" onClick={activeModal.dismiss}>
          Cancel
        </button>
        <button className="btn btn-primary" onClick={handleSave}>
          Save
        </button>
      </div>
    </>
  );
};
```

> This approach is ideal for reusable modal components that need to self-manage close actions.

#### 4. Shaking Animation for Static Modals

To provide visual feedback when a user clicks the backdrop of a static modal (where clicking outside does not close it), you can add a custom animation to your global CSS file.

The library automatically applies the .modal-static-shake class when a static backdrop is clicked.

```css
.modal-static-shake {
  animation: shake 0.3s ease-in-out;
}

@keyframes shake {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.02);
  }
}
```

**Note:** The CSS above is just a reference for the default behavior. You can override the `.modal-static-shake` class with any custom transition or animation you prefer.

## API Reference

This section provides detailed information about the props and configuration options available for the Modal system.

#### 1. The `useModal` Hook

The `useModal` hook returns an object containing methods to control the modal stack programmatically.

| Prop           | Type                                                                                        | Description                                              |
| -------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `open`         | `(component: ModalComponentType, model?: any, options?: ModalOptionsProps) => Promise<any>` | Opens a modal. Resolves with data passed from the modal. |
| `close`        | `(result?: any) => void`                                                                    | Close current active modal                               |
| `dismissAll()` | `() => void `                                                                               | Close all active modals.                                 |

#### 2. Open Parameters

When calling `modal.open()`, you can pass the following arguments to define what to render and how it behaves.

| Prop      | Type                                               | Description                                                                                                    |
| --------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| component | `ReactElement` or `((props: any) => ReactElement)` | The modal component to render. Can be a React element or a component function.                                 |
| model     | `any`                                              | Optional data passed to the modal component as props. Useful for pre-filling forms or passing contextual info. |
| options   | `ModalOptionsProps`                                | Optional modal configuration (size, backdrop, centered, scrollable, fullscreen, custom class).                 |

#### 3. Modal Options (options)

The `options` object is the third argument of the `open()` method. It allows you to customize the behavior and appearance of each specific modal instance.

| Prop           | Type                           | Default | Description                                                              |
| -------------- | ------------------------------ | ------- | ------------------------------------------------------------------------ |
| backdrop       | `boolean` or `static`          | true    | Whether a backdrop is shown. "static" disables closing on click outside. |
| keyboard       | `boolean`                      | false   | Allow ESC key to close modal.                                            |
| size           | `sm`, `lg`, `xl`, `fullscreen` | md      | Sets the modal size.                                                     |
| centered       | `boolean`                      | false   | Vertically center the modal.                                             |
| scrollable     | `boolean`                      | false   | Enable scrolling within modal body.                                      |
| modalClassName | `string`                       | ""      | Add custom CSS class to the modal container                              |

#### 4. The `useActiveModal` Hook

This hook is intended for use inside the component being rendered as a modal (the child component). It provides access to the current modal instance's lifecycle controls.

| Prop         | Type                     | Description                                                                                                  |
| ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `close() `   | `(result?: any) => void` | Closes the modal and returns a success result to the caller. Use this for "Submit," "Save," or "OK" actions. |
| `dismiss() ` | `() => void`             | Closes the modal without a result, typically triggering a cancel or rejection. Use this for "Cancel" buttons |