Source: dialog/Dialog.js

import { Random } from 'meteor/random'
import { ReactiveDict } from 'meteor/reactive-dict'
import './Dialog.html'

/**
 * Dialog components to build animated modal-like dialogs.
 * @module
 * @see https://blazeui.meteorapp.com/components?c=Dialog
 */

/**
 * @typedef DialogState
 * @property open {boolean} mounting state
 * @property visible {boolean} visibility state
 * @property static {boolean} whether clicking backdrop hides dialog (false) or not (true)
 * @property scroll {boolean} whether content can be scrolled
 * @property uid {string} a random unique id for this instance to prefix id attriubutes
 */

/**
 * The root component for dialogs.
 * Provides shared state.
 *
 * @type object
 * @property static {boolean} sets default state.static to the given value
 * @property scroll {boolean} sets default state.scroll to the given value
 */
export const Dialog = {
  name: 'Dialog',
  main: true,
  /**
   * @static
   * @function
   * @return {ReactiveDict}
   */
  state: () => new ReactiveDict({
    open: false,
    visible: false,
    static: false,
    scroll: false,
    uid: Random.id(6)
  }),
  onCreated: ({ instance, state }) => {
    instance.state = state

    const initialStatic = instance.data?.static
    if (typeof initialStatic === 'boolean') {
      instance.state.set({ static: initialStatic })
    }

    const initialScroll = instance.data?.scroll
    if (typeof initialScroll === 'boolean') {
      instance.state.set({ scroll: initialScroll })
    }

    instance.autorun(() => {
      const open = instance.state.get('open')
      if (open) {
        document.body.style.overflowY = 'hidden'
      }
      else {
        document.body.style.overflowY = 'scroll'
      }
    })
  }
}

/** @private */
const useFromContext = () => ({ instance, api }) => {
  const resolve = api.state().useFromContext()
  return resolve({ instance })
}

const onOpen = () => function () { return Template.instance().state.get('open') }

/**
 * Provide any child element that has `type="button"` to trigger the dialog.
 * @type object
 */
export const DialogTrigger = {
  name: 'DialogTrigger',
  class: '',
  state: useFromContext(),
  events: {
    'click [type="button"], click button' (e, t) {
      t.state.set({ open: true, visible: true })
    }
  },
  onCreated: function ({ instance, state }) {
    instance.state = state
  }
}

/**
 * Place the dialog content in here.
 * If `Dialog` is scrollable, then overflowing content will be scrollable, otherwise clipped.
 * @type object
 */
export const DialogContent = {
  name: 'DialogContent',
  class: 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
  state: useFromContext(),
  attributes ({ props, state, api }) {
    const { merge } = api.styles()
    const scrollable = state.scroll && 'max-h-full overflow-y-scroll'
    return {
      role: 'dialog',
      'data-state': state.visible ? 'open' : 'closed',
      'aria-describedby': `${state.uid}-description`,
      'aria-labelledby': `${state.uid}-title`,
      style: 'pointer-events: auto;',
      class: merge(DialogContent.class, scrollable, props.class)
    }
  },
  onCreated: function ({ instance, state }) {
    instance.state = state
  },
  helpers: {
    open: onOpen()
  },
  events: {
    'animationend div' (e, t) {
      if (!t.state.get('visible')) {
        // ensure this will not "flicker"
        e.currentTarget.style.display = 'none'

        if (t.state.get('open')) {
          t.state.set('open', false)
        }
      }
    }
  }
}

/**
 * An optional styled header for the dialog.
 * @type object
 */
export const DialogHeader = {
  name: 'DialogHeader',
  class: 'flex flex-col space-y-1.5 text-center sm:text-left',
  state: useFromContext(),
  onCreated: function ({ instance, state }) {
    instance.state = state
  }
}

/**
 * An optional styled footer for the dialog.
 * @type object
 */
export const DialogFooter = {
  name: 'DialogFooter',
  class: 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
  state: useFromContext(),
  onCreated: function ({ instance, state }) {
    instance.state = state
  }
}

/**
 * An optional styled title for the dialog.
 * @type object
 */
export const DialogTitle = {
  name: 'DialogTitle',
  class: 'text-lg font-semibold leading-none tracking-tight',
  state: useFromContext(),
  attributes ({ props, state, api }) {
    const { merge } = api.styles()
    return {
      id: `${state.uid}-title`,
      class: merge(DialogTitle.class, props.class),
    }
  },
  onCreated: function ({ instance, state }) {
    instance.state = state
  }
}

/**
 * An optional styled description section for the dialog.
 * @type object
 */
export const DialogDescription = {
  name: 'DialogDescription',
  class: 'text-sm text-muted-foreground',
  state: useFromContext(),
  attributes ({ props, state, api }) {
    const { merge } = api.styles()
    return {
      id: `${state.uid}-description`,
      class: merge(DialogDescription.class, props.class),
    }
  },
  onCreated: function ({ instance, state }) {
    instance.state = state
  }
}

/**
 * Dialog close button, which can be used as inline-child or auto-placed top-right.
 * Pass a child element with `role="button"` to trigger closing.
 * @type object
 * @property asChild {boolean=}
 */
export const DialogClose = {
  name: 'DialogClose',
  class: 'absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground',
  attributes ({ props, state, api }) {
    const { merge } = api.styles()
    const clickable = 'cursor-pointer'
    return {
      type: 'button',
      class: merge(!props.asChild && DialogClose.class, clickable, props.class),
    }
  },
  state: useFromContext(),
  events: {
    'click [type="button"], click button' (e, t) {
      t.state.set('visible', false)
    }
  },
  onCreated: function ({ instance, state }) {
    instance.state = state
  }
}

/**
 * The backdrop/overlay in the background.
 * If state.static is not true, then clicking the overlay will close the dialog.
 * @type object
 */
export const DialogOverlay = {
  name: 'DialogOverlay',
  class: 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 duration-1000',
  state: useFromContext(),
  attributes ({ props, state, api }) {
    const { merge } = api.styles()
    return {
      role: 'dialog-overlay',
      'data-state': state.visible ? 'open' : 'closed',
      'data-aria-hidden': !state.visible,
      'aria-hidden': !state.visible,
      style: 'pointer-events: auto;',
      class: merge(DialogOverlay.class, props.class)
    }
  },
  onCreated: function ({ instance, state }) {
    instance.state = state
  },
  helpers: {
    open: onOpen()
  },
  events: {
    'click [role="dialog-overlay"]' (e, t) {
      if (!t.state.get('static')) {
        t.state.set('visible', false)
      }
    },
    'animationend div' (e, t) {
      if (!t.state.get('visible')) {
        // ensure this will not "flicker"
        e.currentTarget.style.display = 'none'
      }
    }
  }
}