@react-above/modal
TypeScript icon, indicating that this package has built-in type declarations

0.3.9 • Public • Published

@react-above/modal

A flexible headless modal library for your React app.

  • All commonly used features (closeOnClickOutside, closeOnEsc, Scroll and Focus locks as plugins)
  • Completely customizable and extendable (actually, it doesn't have an UI out of the box)
  • Lightweight
  • Plugin system (you can even do animations inside)
  • A11y attributes and convenient API for specifying them
  • 2 methods of rendering: children and render-prop
  • A lot of lifecycle methods available
  • Warning: NO full support for multiple/nested modals

Roadmap

  • [x] Core modal features
  • [x] Theme support
  • [x] Plugin system
  • [x] Close on ESC and click outside
  • [ ] Move plugins to separate repos
  • [ ] Theme-level lifecycle callbacks
  • [ ] Animation plugin
  • [ ] Plugin and theme repo templates
  • [ ] Better default theme
  • [ ] Test cases for everything
  • [ ] A small documentation and API reference in READMEs
  • [ ] Release v1.0.0
  • [ ] Logo and banner for react-above
  • [ ] A good comprehensive documentation on a separate website
  • [ ] Theme catalog with demos
  • [ ] Plugin catalog with demos
  • [ ] More themes

Installation

$ yarn add @react-above/modal

Usage

1. Create Modal component

/* Somewhere in your UI layer.. */

import { createModal, ScrollLockPlugin, FocusLockPlugin } from '@react-above/modal'

// you can use any theme instead of the default one
import { ThemeDefault } from '@react-above/modal-theme-default'

export const Modal = createModal({
  theme: ThemeDefault(),
  plugins: [ScrollLockPlugin(), FocusLockPlugin()],
})

2. Use anywhere

Children syntax

The most common usage method. The majority of modal libraries look like so.

import { Modal } from '@app/ui'

<Modal isOpen={isOpen} close={close}>
  <Modal.Surface>
    <Modal.Header title="My modal" close={close} />
    <Modal.Body>My modal description</Modal.Body>
  </Modal.Surface>
</Modal>

Render syntax

Also, react-above's Modal provides the render prop API for more flexibility.

It can be a simple inline-function, or a real React-component - you can freely use hooks inside.

import { Modal } from '@app/ui'

<Modal
  isOpen={isOpen}
  close={close}
  render={({ close }) => (
    <Modal.Surface>
      <Modal.Header title="My modal" close={close} />
      <Modal.Body>My modal description</Modal.Body>
    </Modal.Surface>
  )}
/>

API Reference

Shared types

type Elements = {
  html: HTMLElement
  body: HTMLElement
  screen: HTMLElement
  overlay: HTMLElement
  modal: HTMLElement
}

type LifecycleCallbacks = {
  /*
   * The Modal will wait for Promise to be resolved
   * You can use this behavior to implement animation delays
   */
  onAfterMount?: (elements: Elements) => Promise<void> | void
  onBeforeUnmount?: (elements: Elements) => Promise<void> | void

  /*
   * The DOM-postfixed callbacks are called inside useLayoutEffect
   * If you want to work with HTML nodes - this is the perfect place
   */
  onAfterMountDOM?: (elements: Elements) => void
  onBeforeUnmountDOM?: (elements: Elements) => void
}

type ModalFC = FC<ModalProps>

createModal

type Parameters = {
  /*
   * Theme is used to set up modal's Frame (screen + overlay + modal)
   * Also, Theme can extend Modal component with its specific sub-components,
   * so you can use them like Modal.Surface, Modal.Header, Modal.Body and etc.
   */
  theme: ThemeOutput

  /*
   * Plugins are used to add specific functionality to Modal
   * They have an access to Modal's elements and lifecycle callbacks
   * 
   * Some lifecycle callbacks may be asynchronous,
   * so you can implement animation delay in there
   */
  plugins?: PluginOutput[]

  /*
   * The function returning Modal's render target node
   * It's done as a function for the SSR-compatibility reasons
   */
  root?: () => HTMLElement
}

type ReturnType = ModalFC & {
  // ... custom Theme components
}

Modal

type ModalProps = {
  // no need to explain
  isOpen: boolean

  // "close" callback is used by inner functionality (like "closeOnClickOutside" and "closeOnEsc")
  close: () => void

  // the most common usage method - just pass content as a children
  children?: ReactNode

  /*
   * Render is useful for avoiding the execution of unwanted logic
   * It guarantees that your Renderer component will be called ONLY when modal is open
   * So, you can get rid of useless conditional rendering and hook calls
   * 
   * Also, ModalRenderer may be the common React component,
   * so you can safely use hooks inside
   */
  render?: ModalRenderer

  closeOnClickOutside?: boolean
  closeOnEsc?: boolean

  // title and description for a11y attributes
  aria?: Aria

  // custom root
  root?: () => HTMLElement

  // see in "Shared types" section
  ...LifecycleCallbacks
}

createPlugin

/*
 * Accepts Options type as a generic parameter
 * It defaults to "void"
 * 
 * In "build" callback, "void" transforms to "undefined",
 * to emulate "optional" parameter for better DX
 */
export const MyPlugin = createPlugin<MyPluginOptions | void>({
  build: (options: MyPluginOptions | undefined) => ({
    // LifecycleCallbacks (see in "Shared types" section)
  }),
})

A few words about multiple/nested modals

The main reasons why it's not implemented:

  • In most cases, the nested modals is an anti-pattern
  • It can cause inconvenient public API - most likely you would have to render something like ModalRoot
  • The Overlay shouldn't overlap, so it requires an additional work to be done. We would need a new type of lifecycle callbacks, and the more complicated Theme API
  • It's hard to implement and the resulting code won't look good

The current behavior:

  • You can open multiple Modals at once
  • The Overlay components will overlap - the background will become darker (in case of black transparent Overlay)
  • On click outside: the upper one Modal will be closed
  • On ESC press: ALL Modals will be closed

Readme

Keywords

none

Package Sidebar

Install

npm i @react-above/modal

Weekly Downloads

0

Version

0.3.9

License

MIT

Unpacked Size

23 kB

Total Files

6

Last publish

Collaborators

  • risenforces