0.1.1 • Published 4 months ago

@tipthing/modal-manager v0.1.1

Weekly downloads
-
License
MIT
Repository
github
Last release
4 months ago

Stepped Modal

A lightweight, flexible library for managing multi-step modals in React applications.

Table of Contents

Features

  • 🔄 Multi-step flows within a single modal
  • 🔀 Conditional steps and branching flows
  • 🚪 Intuitive navigation between steps (forward, backward, and direct jumps)
  • 🧠 Smart step history tracking for complex flows
  • 🔍 Built-in debugger for development
  • 📦 Small bundle size with minimal dependencies
  • 📝 Full TypeScript support
  • 🧩 Framework-agnostic (works with any UI library)

Installation

npm install modal-manager
# or
yarn add modal-manager
# or
pnpm add modal-manager

Basic Usage

import { useModal, ModalContent, Step, StepRenderer } from 'modal-manager';
import { Dialog } from 'your-ui-library';

function MyModal() {
  const { isOpen, open, close } = useModal({
    id: 'my-modal',
    steps: [
      { id: 'step1' },
      { id: 'step2' },
      { id: 'step3' },
    ]
  });

  return (
    <>
      <button onClick={open}>Open Modal</button>
      
      <Dialog open={isOpen} onClose={close}>
        <ModalContent id="my-modal">
          {({ currentStep, next, prev, isFirst, isLast }) => (
            <>
              <StepRenderer currentStep={currentStep}>
                <Step id="step1">
                  <h2>Step 1</h2>
                  <p>This is the first step</p>
                </Step>
                
                <Step id="step2">
                  <h2>Step 2</h2>
                  <p>This is the second step</p>
                </Step>
                
                <Step id="step3">
                  <h2>Step 3</h2>
                  <p>This is the final step</p>
                </Step>
              </StepRenderer>
              
              <div className="buttons">
                {!isFirst && <button onClick={prev}>Back</button>}
                {!isLast ? (
                  <button onClick={next}>Next</button>
                ) : (
                  <button onClick={close}>Finish</button>
                )}
              </div>
            </>
          )}
        </ModalContent>
      </Dialog>
    </>
  );
}

Advanced Usage

Conditional Steps and Branching Flows

You can create complex flows where the next step depends on user input:

import { useModal, ModalContent, Step, StepRenderer } from 'modal-manager';
import { Dialog } from 'your-ui-library';

function PaymentModal() {
  const { isOpen, open, close, goTo, setData, data, addStep } = useModal({
    id: 'payment-modal',
    steps: [
      { id: 'method' },
      { id: 'card-details' },
      { id: 'bank-details' },
      { id: 'confirm' },
    ]
  });

  // Set up the step navigation relationships
  useEffect(() => {
    // Define the previous step for each conditional step
    addStep('payment-modal', 'card-details', {}, 'method');
    addStep('payment-modal', 'bank-details', {}, 'method');
    addStep('payment-modal', 'confirm', {}, data.paymentMethod === 'card' ? 'card-details' : 'bank-details');
  }, [addStep, data.paymentMethod]);

  const handlePaymentMethodSelect = (method) => {
    setData({ paymentMethod: method });
    
    // Go to the appropriate step based on payment method
    if (method === 'card') {
      goTo('card-details');
    } else if (method === 'bank') {
      goTo('bank-details');
    }
  };

  return (
    <>
      <button onClick={open}>Make Payment</button>
      
      <Dialog open={isOpen} onClose={close}>
        <ModalContent id="payment-modal">
          {({ currentStep, prev, close }) => (
            <StepRenderer currentStep={currentStep}>
              <Step id="method">
                <h2>Select Payment Method</h2>
                <button onClick={() => handlePaymentMethodSelect('card')}>Credit Card</button>
                <button onClick={() => handlePaymentMethodSelect('bank')}>Bank Transfer</button>
              </Step>
              
              <Step id="card-details">
                <h2>Enter Card Details</h2>
                {/* Card form */}
                <button onClick={() => prev()}>Back</button>
                <button onClick={() => goTo('confirm')}>Continue</button>
              </Step>
              
              <Step id="bank-details">
                <h2>Enter Bank Details</h2>
                {/* Bank form */}
                <button onClick={() => prev()}>Back</button>
                <button onClick={() => goTo('confirm')}>Continue</button>
              </Step>
              
              <Step id="confirm">
                <h2>Confirm Payment</h2>
                <p>Payment Method: {data.paymentMethod}</p>
                <button onClick={() => prev()}>Back</button>
                <button onClick={close}>Confirm</button>
              </Step>
            </StepRenderer>
          )}
        </ModalContent>
      </Dialog>
    </>
  );
}

Smart Navigation History

The library automatically tracks navigation history, making it easy to implement "Back" buttons that work intuitively even in complex flows:

  • When a user navigates forward, the current step is added to history
  • When a user navigates backward, the library uses:
    1. The explicit previousStep if defined for the current step
    2. The navigation history if available
    3. Simple index decrement as a fallback

This ensures users always return to the step they came from, even in non-linear flows.

Using with Different UI Libraries

Material UI

import { useModal, ModalContent } from 'modal-manager';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';

function MaterialUIModal() {
  const { isOpen, open, close } = useModal({
    id: 'mui-modal',
    steps: [{ id: 'step1' }, { id: 'step2' }]
  });

  return (
    <>
      <Button onClick={open}>Open Modal</Button>
      
      <Dialog open={isOpen} onClose={close}>
        <ModalContent id="mui-modal">
          {({ currentStep, next, prev, isFirst, isLast }) => (
            <>
              <DialogTitle>
                {currentStep === 'step1' ? 'Step 1' : 'Step 2'}
              </DialogTitle>
              
              <DialogContent>
                {currentStep === 'step1' ? (
                  <p>Content for step 1</p>
                ) : (
                  <p>Content for step 2</p>
                )}
              </DialogContent>
              
              <DialogActions>
                {!isFirst && <Button onClick={prev}>Back</Button>}
                {!isLast ? (
                  <Button onClick={next}>Next</Button>
                ) : (
                  <Button onClick={close}>Finish</Button>
                )}
              </DialogActions>
            </>
          )}
        </ModalContent>
      </Dialog>
    </>
  );
}

Shadcn/UI

import { useModal, ModalContent } from 'modal-manager';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
  Button
} from '@/components/ui';

function ShadcnModal() {
  const { isOpen, open, close } = useModal({
    id: 'shadcn-modal',
    steps: [{ id: 'step1' }, { id: 'step2' }]
  });

  return (
    <>
      <Button onClick={open}>Open Modal</Button>
      
      <Dialog open={isOpen} onOpenChange={open => !open && close()}>
        <DialogContent>
          <ModalContent id="shadcn-modal">
            {({ currentStep, next, prev, isFirst, isLast }) => (
              <>
                <DialogHeader>
                  <DialogTitle>
                    {currentStep === 'step1' ? 'Step 1' : 'Step 2'}
                  </DialogTitle>
                </DialogHeader>
                
                {currentStep === 'step1' ? (
                  <p>Content for step 1</p>
                ) : (
                  <p>Content for step 2</p>
                )}
                
                <DialogFooter>
                  {!isFirst && <Button variant="outline" onClick={prev}>Back</Button>}
                  {!isLast ? (
                    <Button onClick={next}>Next</Button>
                  ) : (
                    <Button onClick={close}>Finish</Button>
                  )}
                </DialogFooter>
              </>
            )}
          </ModalContent>
        </DialogContent>
      </Dialog>
    </>
  );
}

Debugging

The library includes a built-in debugger component that helps visualize the state of your modals during development:

import { ModalDebugger } from 'modal-manager';

function App() {
  return (
    <>
      {/* Your app components */}
      
      {process.env.NODE_ENV === 'development' && (
        <ModalDebugger position="bottom-right" />
      )}
    </>
  );
}

The debugger shows:

  • All active modals
  • Current step for each modal
  • Step history
  • Modal data
  • Navigation history

API Reference

useModal

const {
  // Actions
  open,        // (data?) => void - Opens the modal with optional initial data
  close,       // () => void - Closes the modal
  next,        // (data?) => void - Goes to the next step with optional data
  prev,        // () => void - Goes to the previous step
  goTo,        // (stepId, data?) => void - Goes to a specific step with optional data
  setData,     // (data) => void - Updates the modal data
  addStep,     // (modalId, stepId, data?, previousStep?) => void - Adds or updates a step
  
  // State
  isOpen,          // boolean - Whether the modal is open
  currentStep,     // string | null - ID of the current step
  currentStepIndex,// number - Index of the current step
  totalSteps,      // number - Total number of steps
  data,            // Record<string, unknown> - Current modal data
  isFirst,         // boolean - Whether the current step is the first step
  isLast,          // boolean - Whether the current step is the last step
} = useModal({
  id,           // string - Unique identifier for the modal
  initialData,  // object - Initial data for the modal (optional)
  steps,        // array - Array of step objects (optional)
});

ModalContent

<ModalContent id="modal-id">
  {({
    currentStep,  // string | null - ID of the current step
    data,         // Record<string, unknown> - Current modal data
    next,         // (data?) => void - Goes to the next step with optional data
    prev,         // () => void - Goes to the previous step
    close,        // () => void - Closes the modal
    goTo,         // (stepId, data?) => void - Goes to a specific step with optional data
    setData,      // (data) => void - Updates the modal data
    isFirst,      // boolean - Whether the current step is the first step
    isLast,       // boolean - Whether the current step is the last step
  }) => (
    // Your modal content
  )}
</ModalContent>

StepRenderer and Step

<StepRenderer currentStep={currentStep}>
  <Step id="step1">
    {/* Content for step 1 */}
  </Step>
  <Step id="step2">
    {/* Content for step 2 */}
  </Step>
</StepRenderer>

ModalDebugger

<ModalDebugger 
  position="bottom-right" // 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
  initiallyOpen={false}   // Whether the debugger is initially open
/>

License

MIT

0.1.1

4 months ago