Modal

‹ Back
Note: This component depends on React.

Modals are implemented using a mixture of react-modal, custom react components and some custom binding wrappers.

Basic Modal

<button data-modal="basic" class="button">Open modal</button>
<%= react_component "Modal", props: {
  id: "basic",
  label: "Sample modal",
  renderHTML: modal_content,
} %>

Available component options

id Event name for use with Ornament.openModal()
label Used in the header and for screenreaders
isOpen Boolean to determine wether modal is open by default
render Render prop when used in react, expects a react component
renderHTML HTML to be rendered inside modal
noHeader Boolean to disable the header
className Custom class for the wrapper element
afterClose Function to call after closing modal
afterOpen Function to call after opening modal

Events

Events can be listened for with document.addEventListener(eventName, func).

  • ornament:modal:{id}:before-open
  • ornament:modal:{id}:after-open
  • ornament:modal:{id}:before-close
  • ornament:modal:{id}:after-close

You can trigger events in the modal by calling events on the document, eg. Ornament.fireEvent(eventName).

  • ornament:modal:{id}:open
  • ornament:modal:{id}:close

Sizing

Using the size prop, you can change the size of your modal.

Sizes are defined entirely in CSS, check out the modal.scss partial to configure your sizes.

Ornament by default comes with these sizes:

<button data-modal="size-small" class="button">Open small modal</button>
<%= react_component "Modal", props: {
  id: "size-small",
  label: "Small modal",
  size: "small",
  renderHTML: modal_content,
} %>
<%button data-modal="size-large" class="button">Open large modal<%/button>
<%%= react_component "Modal", props: {
  id: "size-large",
  label: "Large modal",
  size: "large",
  renderHTML: modal_content,
} %>
<button data-modal="size-fullscreen" class="button">Open fullscreen modal</button>
<%= react_component "Modal", props: {
  id: "size-fullscreen",
  label: "Fullscreen modal",
  size: "fullscreen",
  renderHTML: modal_content,
} %>

All modals with the exception of videos and ajax loading/error states will go fullscreen when they hit a breakpoint.

This behaviour can be disabled globally by setting the sass variable $fullscreen-mobile to false.

This behaviour can also be disabled as a prop with noFullscreen: true.


Ajax content

A common pattern is to load in an ajax endpoint and inject the resulting HTML in to a modal. This can be accomplished by using data-modal-ajax="url" on your button. This requires no modal component as it will use a global ajax-ready modal.

<button data-modal-ajax="/styleguide/getting_started" class="button">Load modal with ajax content</button>

You can programatically load content in to the ajax modal with Ornament.C.Modal.openAjaxModal(url);.

Sizing

You can set the size of ajax modals using data-modal-size:

<button data-modal-ajax="/styleguide/getting_started" data-modal-size="small" class="button">Load small ajax modal</button>

Error state

<button data-modal-ajax="/bad-url" class="button">Load modal with ajax content</button>

Templating ajax views

Any URLs loaded in to the ajax modal will get an extra URL parameter modal=true, you can use this parameter to render the resulting view with the modal rails layout.

If using Koi, I'd recommend modifying the layout_by_resource method in common_controller_actions to check for this param and return the modal layout.

This layout provides the header and body structure for seamless modal integration.

Keep in mind that if you customise the layout of ControlledModal.jsx, you'll likely want to update layouts/modal.html.erb to match.

Ajaxed components

Be aware that if your ajaxed content has javascript components you may need to reinitialise them.

This includes react components, which can be remounted using ReactOnRails.reactOnRailsPageLoaded();.

Most Ornament components have an init function you can call, eg. Ornament.C.FormsEnhanced.init().


Videos

You can use a video prop instead of the other render props to load in a responsive iframe:

<button data-modal="video-youtube-iframe" class="button">Open video from youtube</button>
<%= react_component "Modal", props: {
  id: "video-youtube-iframe",
  label: "Video",
  video: "https://www.youtube.com/embed/6ZfuNTqbHE8",
} %>

This prop is strictly an iframe and can be any sort of iframe. The only limitation is that it's wrapped in a responsive video wrapper so it's aspect ratio will be that of a video. For example here is one from vimeo.

<button data-modal="video-vimeo-iframe" class="button">Open video from vimeo</button>
<%= react_component "Modal", props: {
  id: "video-vimeo-iframe",
  label: "Video",
  video: "https://player.vimeo.com/video/112866269",
} %>

With Youtube id

If you're using a youtube video, you can use the youtube video ID and take advantage of the pre-baked youtube configuration by using youtube prop instead.

Using noHeader is recommended for a more cinematic experience, but left optional.

<button data-modal="video-youtube" class="button">Open video from youtube</button>
<%= react_component "Modal", props: {
  id: "video-youtube",
  label: "Video",
  youtube: "6ZfuNTqbHE8",
  noHeader: true,
} %>

Videos are automatically ratio sized to 16x9, you can specify a custom ratio as a string with the ratio prop.


Custom close buttons

You can create a custom close button by adding data-modal-close on any button.

<button data-modal="custom-close" class="button">Open modal</button>
<%= react_component "Modal", props: {
  id: "custom-close",
  label: "Custom close button",
  renderHTML: modal_content + "<div class='panel--padding content'><button class='button__cancel' data-modal-close>Custom close button</button></div>".html_safe,
} %>

Linking Modals

Add data-modal-link to your button to add close any open modals, wait, then open the requested modal.

<button data-modal="linking-one" class="button">Open modal</button>
<%= react_component "Modal", props: {
  id: "linking-one",
  label: "Modal #1",
  renderHTML: "<div class='panel--padding content'><button class='button' data-modal='linking-two' data-modal-link>Go to modal #2</button></div>".html_safe,
} %>
<%= react_component "Modal", props: {
  id: "linking-two",
  label: "Modal #2",
  renderHTML: "<div class='panel--padding content'><button class='button' data-modal='linking-one' data-modal-link>Go to modal #1</button> or <button class='button__cancel' data-modal-close>Close button</button></div>".html_safe,
} %>

Footers

You can provide a fixed footer using either HTML string in footerHTML or a react component in footer.

<button data-modal="with-footer" class="button">Open modal</button>
<%= react_component "Modal", props: {
  id: "with-footer",
  label: "Modal with footer",
  footerHTML: footer_content,
  renderHTML: modal_content,
} %>

React compositing

Sometimes you need really complex javascript-driven modals, or modals inside react components.

Here is a very simple example of creating a user component that shows user details in a modal using a controlled modal, local state and render props:

import React from "react";
import ControlledModal from '../Modal/ControlledModal';

export default class UserListItem extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      modalOpen: false,
    }
  }

  openModal = () => {
    this.setState({
      modalOpen: true,
    });
  }

  closeModal = () => {
    this.setState({
      modalOpen: false,
    })
  }

  render(){
    <div>
      <strong>{this.props.userName}</strong>
      <button onclick={this.openModal}>View more details</button>
      <ControlledModal
        open={this.state.modalOpen}
        onClose={this.props.closeModal}
        className="ReactModal__User"
        render={() => (
          <div>
            <h2>{this.props.userName}</h2>
            <div className="content" dangerouslySetInnerHTML={{__html: this.props.userContent }} />
          </div>
        )}
      </>
    </div>
  }

}

ShadowScroller Component

The custom react modal uses a custom content wrapper component called ShadowScroller, which automatically adds shadows to the top and bottom of scrollable areas when the content is overflowed.

ShadowScroller can be utilised for other scrollable areas in two ways:

React ref

If your scrollable div is in React land, you can use the scrollRef function to target the scrollable area:

<ShadowScroller render={(scrollRef) => (
  <div ref={el => scrollRef(el)}>
    Your scrollable content
  </div>
)} />

DOM selector

However, if your scrollable area is not in React land, say you're using dangerouslySetInnerHTML to dump a bunch of HTML in a react component and THAT has a scrollable area in it, you can use the second parameter of the wrapper component, domRef to target a selector:

<ShadowScroller render={(scrollRef, domRef) => (
  <div
    ref={el => domRef(el, ".my-scrollable-area")}
    dangerouslySetInnerHTML={{__html: this.props.htmlContent}}
  ></div>
)} />