Skip to content

Latest commit

 

History

History
232 lines (179 loc) · 5.6 KB

File metadata and controls

232 lines (179 loc) · 5.6 KB

TpModal

A fully accessible, headless modal component with compound component pattern.

Features

  • ✅ Controlled and uncontrolled modes
  • ✅ Focus trap and focus management
  • ✅ Body scroll lock
  • ✅ Keyboard navigation (Escape to close)
  • ✅ Click outside to close
  • ✅ Portal rendering to document.body
  • ✅ Full ARIA support
  • ✅ TypeScript first

Basic Usage

import {
  TpModal,
  TpModalOverlay,
  TpModalContent,
  TpModalClose,
  TpModalTitle,
  TpModalDescription,
} from '@travelopia/react-components'

function MyComponent() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>

      <TpModal open={isOpen} onOpenChange={setIsOpen}>
        <TpModalOverlay>
          <TpModalContent>
            <TpModalTitle>Modal Title</TpModalTitle>
            <TpModalDescription>
              This is the modal description.
            </TpModalDescription>
            <TpModalClose>
              <button>Close</button>
            </TpModalClose>
          </TpModalContent>
        </TpModalOverlay>
      </TpModal>
    </>
  )
}

Uncontrolled Mode

<TpModal defaultOpen={false}>
  <button onClick={() => {/* open logic */}}>Open</button>
  <TpModalOverlay>
    <TpModalContent>
      <h2>Uncontrolled Modal</h2>
      <TpModalClose><button>Close</button></TpModalClose>
    </TpModalContent>
  </TpModalOverlay>
</TpModal>

With Custom Hook

import { useTpModal } from '@travelopia/react-components'

function MyComponent() {
  const modal = useTpModal()

  return (
    <>
      <button onClick={modal.open}>Open Modal</button>

      <TpModal open={modal.isOpen} onOpenChange={(open) => !open && modal.close()}>
        <TpModalOverlay>
          <TpModalContent>
            <h2>Modal Content</h2>
            <button onClick={modal.close}>Close</button>
          </TpModalContent>
        </TpModalOverlay>
      </TpModal>
    </>
  )
}

Styling with Tailwind CSS

<TpModal open={isOpen} onOpenChange={setIsOpen}>
  <TpModalOverlay className="bg-black/80 backdrop-blur-sm">
    <TpModalContent className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
      <TpModalTitle className="text-2xl font-bold mb-4">
        Styled Modal
      </TpModalTitle>
      <TpModalDescription className="text-gray-600 mb-6">
        This modal is styled with Tailwind CSS.
      </TpModalDescription>
      <TpModalClose className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
        <span>Close</span>
      </TpModalClose>
    </TpModalContent>
  </TpModalOverlay>
</TpModal>

API Reference

TpModal

Root component that manages modal state.

Prop Type Default Description
open boolean - Controlled open state
defaultOpen boolean false Default open state (uncontrolled)
onOpenChange (open: boolean) => void - Callback when open state changes
children ReactNode - Modal content

TpModalOverlay

The modal backdrop/overlay.

Prop Type Default Description
closeOnClick boolean true Whether clicking overlay closes modal
className string - Custom CSS class
children ReactNode - Usually contains TpModalContent

TpModalContent

The modal dialog box.

Prop Type Default Description
className string - Custom CSS class
children ReactNode - Modal content

ARIA Attributes:

  • role="dialog"
  • aria-modal="true"
  • aria-labelledby - Links to TpModalTitle
  • aria-describedby - Links to TpModalDescription

TpModalClose

Close button wrapper.

Prop Type Default Description
aria-label string "Close modal" Accessible label
className string - Custom CSS class
children ReactNode - Button content

TpModalTitle

Modal title (for accessibility).

Prop Type Default Description
as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' 'h2' Heading level
className string - Custom CSS class
children ReactNode - Title text

TpModalDescription

Modal description (for accessibility).

Prop Type Default Description
className string - Custom CSS class
children ReactNode - Description text

Hooks

useTpModal

Custom hook to manage modal state.

const modal = useTpModal(defaultOpen?: boolean)

Returns:

{
  isOpen: boolean
  open: () => void
  close: () => void
  toggle: () => void
}

Example:

const modal = useTpModal(false)

<button onClick={modal.open}>Open</button>
<button onClick={modal.close}>Close</button>
<button onClick={modal.toggle}>Toggle</button>

Accessibility

Keyboard Navigation

  • Escape: Close modal
  • Tab: Cycle through focusable elements (trapped within modal)

Focus Management

  • Focus is automatically trapped within the modal when open
  • Focus returns to the trigger element when closed
  • First focusable element receives focus on open

Screen Reader Support

  • Proper ARIA attributes for dialog role
  • Title and description are linked for screen readers
  • Modal state is announced when opened/closed

Performance

  • Portal rendering prevents z-index issues
  • Body scroll lock prevents background scrolling
  • Minimal re-renders with optimized context
  • Tree-shakeable - import only what you need