Dialog component for React

July 31, 2019 ยท View on GitHub

Back to Polythene Dialog main page

Dialog component for React

Options

Dialog options

Usage

Calling a Dialog

Other than most other components, Dialog is not rendered directly but invoked through function calls show and hide.

Dialog spawner

Dialogs will be spawned from the component invocation (<Dialog />). To show a dialog instance, use Dialog.show() - more on that later.

Because a dialog should float on top of everything else, outside of the context of the caller, it can be considered a global component. It is best placed in the root view, so that it is not obstructed by other components:

import React from "react"
import { Dialog } from "polythene-react"

// ...
render() {
  return (<div>
    // ... app content
    <Dialog  />
  </div>)
}

The Dialog component itself does not accept any appearance options. Instead, you pass options when calling show - allowing to show custom dialogs from anywhere in the app.

Styling side notes:

  • Writing the dialog at the bottom makes for less surprises (instead of using CSS only for positioning); Mobile Safari sometimes has surprises with position: fixed, so placing it here will most likely work as intended.
  • The order of elements in the root view may differ - CSS attribute z-index is set higher than other content.

Multiple dialog spawners

Usually you'll use only one location for dialogs - on top of all content and centered on screen - but it is possible to have a dialog instance spawned from a different location.

When you are using multiple spawners, differentiate them with option spawn:

<Dialog spawn="special" />

Calls to show the that particular dialog will then also need to pass the same spawn name:

Dialog.show(dialogOptions, { spawn: "special" })

Multiple dialogs

Multiple dialogs can co-exist for the same spawn. Add a unique id to each dialog. When using an array of dialogs, differentiate with unique keys.

Showing and hiding dialogs

Dialog functions:

Dialog.show(options)
Dialog.hide(options)

show

Shows a new dialog instance.

Dialog.show(dialogOptions, spawnOptions) : Promise

ParameterRequiredTypeDefaultDescription
dialogOptionsrequiredOptions object or Function that returns an options objectSee Dialog options
ParameterRequiredTypeDefaultDescription
spawnOptions.idoptionalString"default_dialog"Dialog instance id; use to differentiate simultaneous dialogs.
spawnOptions.spawnoptionalString"default_dialog"Spawn id. Use with multiple spawn locations. spawn must also be passed as option at the spawning Dialog.

Examples:

const dialogOptions = {
  body: "some text"
}

// variations:
Dialog.show(dialogOptions)
Dialog.show(dialogOptions, { id: "confirm" })
Dialog.show(dialogOptions, { spawn: "special" })
Dialog.show(dialogOptions).then(() => console.log("dialog shown"))

Calling show a second time with the same id will redraw the dialog with new options:

Dialog.show(
  { title: "Log in" },
  { id: "login" }
)

// some time later:
Dialog.show(
  { title: "Log in again" },
  { id: "login" }
)

hide

Hides the current dialog instance.

Dialog.hide(spawnOptions) : Promise

ParameterRequiredTypeDefaultDescription
spawnOptions.idoptionalStringDialog instance id; use to differentiate simultaneous dialogs.
spawnOptions.spawnoptionalStringSpawn id. Use with multiple spawn locations. spawn must also be passed as option at the spawning Dialog.

Examples:

Dialog.hide()
Dialog.hide({ id: "confirm" })
Dialog.hide({ spawn: "special" })
Dialog.hide().then(() => console.log("dialog hidden"))

Callbacks

Two optional callback options can be used after the transition: didShow and didHide. Useful when a route change is needed after the dialog is displayed or hidden:

const dialogOptions = {
  didHide: id => history.push("/")
}

Drawing a Dialog

A dialog pane consist of the elements:

  • header
  • body
  • footer

Variations:

  • The header can be substibuted with convenience option title: this draws the title text according to Material Design specs.
  • The footer can be substibuted with convenience option footerButtons: this accepts an array of buttons and will be drawn right-aligned according to Material Design specs.
  • Use fullBleed to remove the padding from the body area
  • Use borders to conditionally add a top and bottom border to the body

A dialog header can contain any content, but using a Toolbar is convenient to display action buttons (not according to Material Design specs, but nonetheless used in many interfaces).

import React from "react"
import { Dialog, Toolbar, ToolbarTitle } from "polythene-react"

const dialogOptions = {
  header: <Toolbar><ToolbarTitle>Title</ToolbarTitle></Toolbar>,
  body: "Body",
  footer: <Toolbar><ToolbarTitle>Footer</ToolbarTitle></Toolbar>,
}

Dialog.show(dialogOptions)

Example with modal and backdrop

A modal dialog is a dialog that can only be closed with an explicit choice; clicking the background does not count as a choice.

To make this behavior explicit, a modal dialog often has a tinted backdrop. This also gives focus to the dialog contents.

import React from "react"
import { Dialog } from "polythene-react"

const footerButtons = [
  /* Note that we are passing JSX elements in an array, hence the comma separator and keys */
  <Button key="cancel" label="Cancel" events={{ onClick: () => Dialog.hide() }} />,
  <Button key="discard" label="Discard" events={{ onClick: () => Dialog.hide() }} />
]

const dialogOptions = {
  body: "Discard draft?",
  modal: true,
  backdrop: true,
  footerButtons
}

Dialog.show(dialogOptions)

Full screen dialog

A full screen dialog uses Toolbar to implement its own header (options title and footer are not used):

import React from "react"
import { Dialog, Button, Toolbar, IconButton } from "polythene-react"
import { addLayoutStyles } from "polythene-css"

addLayoutStyles() // to use <span className="flex" />

const DIALOG_CONFIRM = "confirm-fullscreen"
const iconClose = <svg width="24" height="24" viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
const shortText = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

const BodyText = () => (
  <div>
    {[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(num => <p key={num}>{shortText}</p>)}
  </div>
)

// Second dialog
const confirmDialogOpts = ({
  body: "This event is not yet saved. Are you sure you want to delete this event?",
  modal: true,
  backdrop: true,
  footerButtons: [
    <Button
      label="Cancel"
      events={{
        onClick: () => Dialog.hide({ id: DIALOG_CONFIRM })
      }}
    />,
    <Button
      label="Delete"
      events={{
        onClick: () => (
          
          Dialog
            .hide({ id: DIALOG_CONFIRM }) // hide confirm dialog
            .then(Dialog.hide) // hide main dialog
        )
      }}
    />
  ],
})

Dialog.show({
  fullScreen: true,
  backdrop: true,
  header: <Toolbar content={toolbarRow("New event")} />,
  body: <BodyText />
})

Dynamic content

There are 2 ways to keep the dialog contents up to date:

  1. By passing dialog options as a function.
  2. By continuously calling Dialog.show(attrs) with possibly changed attrs.

Examples of both are shown below.

Passing dialog options as a function

When using static dialog content, passing a POJO as dialog options to Dialog.show works just fine. This falls short when the content needs to be updated with outside state changes. By passing the options as a function, you ensure that the options are read afresh with the new state:

const optionsFn = () => {
  return {
    body: "some text"
  }
}

Dialog.show(optionsFn)

The more elaborate example below shows a file upload form, where the submit button is disabled until a file has been selected.

import React, { Component } from "react"
import { Button, Dialog, DialogPane } from "polythene-react"

class ConditionalDialogPane extends Component {
  constructor(props) {
    super(props)
    this.state = {
      file: undefined
    }
  }
  render() {
    const disabled = this.state.file === undefined
    return (
      <DialogPane
        title="Select a file..."
        body={<input
          type="file"
          id="file"
          name="file"
          onChange={e => this.setState({file: e.target.value})}
        />}
        formOptions={{
          name: "demo",
          method: "post",
          encType: "multipart/form-data",
          onSubmit: e => {
            e.preventDefault()
            alert("Posted: " + this.state.file)
            Dialog.hide()
            this.setState({file: null})
          }
        }}
        footerButtons={<div>
          <Button
            label="Cancel"
            events={{
              onClick: () => Dialog.hide()
            }}
          />
          <Button
            disabled={disabled}
            label="Post"
            type="submit"
            element="button"
            events={{
              onClick: () => Dialog.hide()
            }}
          />
        </div>}
        didHide={() => this.setState({file: null})}
      />
    )
  }
}

Dialog.show({
  panes: [<ConditionalDialogPane />]  
})

Continuously calling Dialog.show

The example shows a counter that is reflected in the dialog.

import React, { Component } from "react"
import { Dialog, Button } from "polythene-react"

const longText = <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>

class Updating extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0,
      dialogVisible: false
    }
    setInterval(() => this.setState({ count: this.state.count + 1 }), 1000)
  }

  componentDidUpdate() {
    if (this.state.dialogVisible) {
      const dialogProps = {
        title: this.state.count,
        body: longText,
        didHide: () => this.setState({ dialogVisible: false })
      }
      Dialog.show(dialogProps)
    }
  }

  render () {
    return <div>
      {this.state.count}
      <Button
        raised
        label="Show Dialog"
        events={{
          onClick: () => this.setState({ dialogVisible: !this.state.dialogVisible })
        }}
      />
    </div>
  }
}

Appearance

Styling

Below are examples how to change the Dialog appearance, either with a theme or with CSS.

You can find more information about theming in Theming.

Themed component

import { DialogCSS } from "polythene-css"

DialogCSS.addStyle(".themed-dialog", {
  color_light_content_background: "#2196F3",
  color_light_body_text: "#fff",
  border_radius: 5
})

<Dialog className="themed-dialog" />

CSS

Change CSS using:

Class names can be imported with:

import classes from "polythene-css-classes/dialog"
import classes from "polythene-css-classes/dialog-pane"

Style

Some style attributes can be set using option style. For example:

Dialog.show({
  style: {
    background: "#fff59d",
    padding: "1.5rem"
  }
})

RTL (right-to-left) support

The direction of Dialog content is reversed when the dialog element either:

  • has attribute dir="rtl"
  • has className pe-rtl

Dark or light tone

If the component - or a component's parent - has option tone set to "dark", the component will be rendered with light colors on dark.

  • Use tone: "dark" to render light on dark
  • Use tone: "light" to locally render normally when dark tone is set

Transitions

See Transitions