Repository: mapbox/mr-ui (ts)

Issue:
MbxAlertModal component
We could consolidate some minor variation between apps to create a single component for alert modals. 

Alert modals prompt the user for confirmation and sometimes allow for cancellation. Typically we include an illustration, text, and buttons, all center-aligned.

In both Studio and Account Dashboard, the cancel button is on the left, confirm button on the right. In both places, the confirm button is red if the action is destructive (deleting something). But the two apps use different illustrations, and slightly different buttons:

<img width="401" alt="screen shot 2018-07-27 at 10 51 31 am" src="https://user-images.githubusercontent.com/628431/43338768-4648d7fa-918c-11e8-9b6a-ab54b3cebba3.png">

<img width="676" alt="screen shot 2018-07-27 at 11 02 14 am" src="https://user-images.githubusercontent.com/628431/43338878-8fe6827c-918c-11e8-8815-5d987af77f29.png">

In some cases you're just acknowledging that something went wrong, so there's no close button in the upper right and your only option is to say "Okay" with resignation. (Illustration is missing below because I blocked network traffic to get the error.)

<img width="397" alt="screen shot 2018-07-27 at 11 12 14 am" src="https://user-images.githubusercontent.com/628431/43343168-17a99e76-919a-11e8-9a6e-113490ac7351.png">

We do have this one weird case where there are 3 actions:

<img width="418" alt="screen shot 2018-07-27 at 10 50 41 am" src="https://user-images.githubusercontent.com/628431/43339129-5def2c82-918d-11e8-9c2d-14abc4c92790.png">

Here's how I'd propose we make a generic component:

- Use the knob-monster illustration until somebody tells us not to.
- Use the button styles proposed in https://github.com/mapbox/mr-ui/issues/11
- Allow for primary, secondary, and tertiary actions. If there's a tertiary action, follow @emilymdubois's suggestion of adjusting alignment like this:
  <img width="603" alt="screen shot 2018-07-27 at 12 40 51 pm" src="https://user-images.githubusercontent.com/628431/43343221-5c12083c-919a-11e8-852b-55971f71e69e.png">
- Allow for a title — like "Delete account," above — beneath the illustration.
- Ensure that the Enter key performs the primary action and Escape closes the modal.
- Try our best to follow these accessibility authoring practices for alert modals: https://www.w3.org/TR/2018/WD-wai-aria-practices-1.2-20180719/examples/dialog-modal/alertdialog.html

See also @emilymdubois's tickets https://github.com/mapbox/studio/issues/9468 and https://github.com/mapbox/studio/issues/9462

Pull request:
Modal adjustments
Closes #12, but does *not* do so by creating an AlertModal component. Instead, I decided to rework the Modal and add action and `alert` props to it. If you provide `primaryAction`, `secondaryAction`, and `tertiaryAction`, those are rendered in a standard button arrangement at the bottom of the modal.

Here are the items I put in the changelog:

- **Modal**
  - 🚨 [breaking change] Remove `underlayStyle`, `underlayProps`, `themeUnderlay`, `themeModal`, and `themeButtonClose` props.
  - 🚨 [breaking change] Rename `title` to `accessibleTitle`.
  - [feature] Add `padded`, `alert`, `primaryAction`, `secondaryAction`, and `tertiaryAction` props.
  - [fix] Modals taller than the viewport will now show padding above them: the browser will not longer automatically scroll down to the top of the modal.
  - [fix] Update `react-aria-modal` to get some bug fixes and focus-management improvements.

@kepta would you please review this?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e7d5f1..4f5cff8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,12 @@
   - 🚨 [breaking change] Remove `backgroundColor` and `themeTooltip` props.
   - [feature] Add `coloring`, `padded`, and `maxWidth` props.
   - [feature] Children of Tooltip no longer need to be functions! If you provide a standard DOM element (e.g. `<button>`, `<div>`, `<span>`) or a Button component, things will just work. You can still use a function if your trigger is a custom component.
+- **Modal**
+  - 🚨 [breaking change] Remove `underlayStyle`, `underlayProps`, `themeUnderlay`, `themeModal`, and `themeButtonClose` props.
+  - 🚨 [breaking change] Rename `title` to `accessibleTitle`.
+  - [feature] Add `padded`, `alert`, `primaryAction`, `secondaryAction`, and `tertiaryAction` props.
+  - [fix] Modals taller than the viewport will now show padding above them: the browser will not longer automatically scroll down to the top of the modal.
+  - [fix] Update `react-aria-modal` to get some bug fixes and focus-management improvements.
 
 ## 0.2.1
 
diff --git a/package-lock.json b/package-lock.json
index bec4498..55ba757 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6923,26 +6923,11 @@
       }
     },
     "focus-trap-react": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-3.1.2.tgz",
-      "integrity": "sha512-MoQmONoy9gRPyrC5DGezkcOMGgx7MtIOAQDHe098UtL2sA2vmucJwEmQisb+8LRXNYFHxuw5zJ1oLFeKu4Mteg==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-4.0.0.tgz",
+      "integrity": "sha512-wUuOylJyxBaQOm67MKiGcOZ/71wuz2GjPMjaWlx9vZHQMQfH2tVbwPB8LteHysaWE04Wc18F/p+YLMkaf3YseA==",
       "requires": {
-        "focus-trap": "^2.0.1"
-      },
-      "dependencies": {
-        "focus-trap": {
-          "version": "2.4.6",
-          "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-2.4.6.tgz",
-          "integrity": "sha512-vWZTPtBU6pBoyWZDRZJHkXsyP2ZCZBHE3DRVXnSVdQKH/mcDtu9S5Kz8CUDyIqpfZfLEyI9rjKJLnc4Y40BRBg==",
-          "requires": {
-            "tabbable": "^1.0.3"
-          }
-        },
-        "tabbable": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-1.1.3.tgz",
-          "integrity": "sha512-nOWwx35/JuDI4ONuF0ZTo6lYvI0fY0tZCH1ErzY2EXfu4az50ZyiUX8X073FLiZtmWUVlkRnuXsehjJgCw9tYg=="
-        }
+        "focus-trap": "^3.0.0"
       }
     },
     "for-in": {
@@ -13924,12 +13909,12 @@
       }
     },
     "react-aria-modal": {
-      "version": "2.12.2",
-      "resolved": "https://registry.npmjs.org/react-aria-modal/-/react-aria-modal-2.12.2.tgz",
-      "integrity": "sha512-H+i1H+GOZ0ZRRIfvyLl+RXkyK6V3WO+Y3IlsEbXxlNwgoaI7hxfnvu+rgsqtWT0u2CcSg+KFkdSSyIA3wD8Rog==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/react-aria-modal/-/react-aria-modal-3.0.0.tgz",
+      "integrity": "sha512-qudZTYYkrahJIPXssuI/MYQiskkIg4pW+3eVI1mPCn3XbNQ2eun7/3ghVV4IPYTSXERRX9LVQbbfmoFu2Ie9Gg==",
       "requires": {
-        "focus-trap-react": "^3.0.4",
-        "no-scroll": "^2.0.0",
+        "focus-trap-react": "^4.0.0",
+        "no-scroll": "^2.1.1",
         "react-displace": "^2.3.0"
       }
     },
diff --git a/package.json b/package.json
index 58ba609..8dae9ec 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
     "hoverintent": "^2.0.0",
     "os-key": "^1.0.0",
     "prefix": "^1.0.0",
-    "react-aria-modal": "^2.10.0",
+    "react-aria-modal": "^3.0.0",
     "react-datepicker": "^0.60.2",
     "react-displace": "^2.3.0",
     "react-submittable": "^2.0.0",
diff --git a/src/components/modal/examples/modal-a.js b/src/components/modal/examples/modal-a.js
new file mode 100644
index 0000000..301202f
--- /dev/null
+++ b/src/components/modal/examples/modal-a.js
@@ -0,0 +1,58 @@
+/*
+The standard modal.
+*/
+import React from 'react';
+import Modal from '../modal';
+import Button from '../../button';
+
+export default class Example extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = { modalOpen: false };
+    this.openModal = this.openModal.bind(this);
+    this.closeModal = this.closeModal.bind(this);
+  }
+
+  openModal() {
+    this.setState({ modalOpen: true });
+  }
+
+  closeModal() {
+    this.setState({ modalOpen: false });
+  }
+
+  renderModal() {
+    if (!this.state.modalOpen) {
+      return null;
+    }
+    return (
+      <Modal
+        accessibleTitle="Animals"
+        onExit={this.closeModal}
+        primaryAction={{ text: 'Okay', callback: this.closeModal }}
+        secondaryAction={{ text: 'Not sure', callback: this.closeModal }}
+      >
+        <div className="prose">
+          <h4>Animals</h4>
+          <ul>
+            <li>Humpback whale</li>
+            <li>Rufous hummingbird</li>
+            <li>Sea otter</li>
+            <li>Snowshoe hare</li>
+          </ul>
+        </div>
+      </Modal>
+    );
+  }
+
+  render() {
+    return (
+      <div>
+        <Button size="medium" onClick={this.openModal}>
+          Show modal
+        </Button>
+        {this.renderModal()}
+      </div>
+    );
+  }
+}
diff --git a/src/components/modal/examples/modal-b.js b/src/components/modal/examples/modal-b.js
new file mode 100644
index 0000000..e9fa776
--- /dev/null
+++ b/src/components/modal/examples/modal-b.js
@@ -0,0 +1,71 @@
+/*
+A small modal, unpadded to allow for custom background coloring, with
+initial focus set on an input. Also features its own action special button.
+*/
+import React from 'react';
+import Modal from '../modal';
+import Button from '../../button';
+
+export default class Example extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = { modalOpen: false };
+    this.openModal = this.openModal.bind(this);
+    this.closeModal = this.closeModal.bind(this);
+  }
+
+  openModal() {
+    this.setState({ modalOpen: true });
+  }
+
+  closeModal() {
+    this.setState({ modalOpen: false });
+  }
+
+  renderModal() {
+    if (!this.state.modalOpen) {
+      return null;
+    }
+    return (
+      <Modal
+        accessibleTitle="Animals"
+        onExit={this.closeModal}
+        padded={false}
+        size="small"
+        initialFocus="#modal-input"
+      >
+        <h2 className="px24 py24 bg-gray-dark color-white txt-h4 txt-fancy round-t">
+          Animals!
+        </h2>
+        <div className="px24 py24">
+          <div className="prose">
+            <p>Type an animal name. Here are some suggestions:</p>
+            <ul>
+              <li>Humpback whale</li>
+              <li>Rufous hummingbird</li>
+              <li>Sea otter</li>
+              <li>Snowshoe hare</li>
+            </ul>
+          </div>
+          <div className="align-center mt24">
+            <input className="input mb12" id="modal-input" />
+            <Button size="medium" onClick={this.closeModal}>
+              Ok, done
+            </Button>
+          </div>
+        </div>
+      </Modal>
+    );
+  }
+
+  render() {
+    return (
+      <div>
+        <Button size="medium" onClick={this.openModal}>
+          Show modal
+        </Button>
+        {this.renderModal()}
+      </div>
+    );
+  }
+}
diff --git a/src/components/modal/examples/modal-example-basic.js b/src/components/modal/examples/modal-example-basic.js
deleted file mode 100644
index 2779c20..0000000
--- a/src/components/modal/examples/modal-example-basic.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-Basic.
-*/
-import React from 'react';
-import Modal from '../modal';
-
-export default class Example extends React.Component {
-  state = { modalOpen: false };
-
-  openModal = () => {
-    this.setState({ modalOpen: true });
-  };
-
-  closeModal = () => {
-    this.setState({ modalOpen: false });
-  };
-
-  render() {
-    return (
-      <div>
-        <button className="btn btn--s bg-pink" onClick={this.openModal}>
-          Show modal
-        </button>
-        {this.state.modalOpen && (
-          <Modal title="Hello explorer" onExit={this.closeModal}>
-            <div className="prose">
-              <h4>Animals</h4>
-              <ul>
-                <li>Humpback whale</li>
-                <li>Rufous hummingbird</li>
-                <li>Sea otter</li>
-                <li>Snowshoe hare</li>
-              </ul>
-            </div>
-          </Modal>
-        )}
-      </div>
-    );
-  }
-}
diff --git a/src/components/modal/examples/modal-example-options.js b/src/components/modal/examples/modal-example-options.js
deleted file mode 100644
index 7932722..0000000
--- a/src/components/modal/examples/modal-example-options.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
-Options.
-*/
-import React from 'react';
-import Modal from '../modal';
-import ControlText from '../../control-text';
-
-export default class Example extends React.Component {
-  state = { modalOpen: false };
-
-  openModal = () => {
-    this.setState({ modalOpen: true });
-  };
-
-  closeModal = () => {
-    this.setState({ modalOpen: false });
-  };
-
-  render() {
-    return (
-      <div>
-        <button className="btn btn--s bg-pink" onClick={this.openModal}>
-          Show modal
-        </button>
-        {this.state.modalOpen && (
-          <Modal
-            title="Hello explorer"
-            onExit={this.closeModal}
-            size="auto"
-            initialFocus="[data-animal-input-field]"
-            underlayStyle={{
-              transform: 'scale(0.9)'
-            }}
-            themeModal="bg-white px36 py36 round border border--pink"
-            themeButtonClose="'btn btn--transparent unround-t unround-br color-pink py12 px12'"
-          >
-            <div className="prose">
-              <h4>Animals</h4>
-              <ul>
-                <li>Humpback whale</li>
-                <li>Rufous hummingbird</li>
-                <li>Sea otter</li>
-                <li>Snowshoe hare</li>
-              </ul>
-              <ControlText
-                data-animal-input-field={true}
-                id="name"
-                placeholder="Suggest another animal"
-                onChange={
-                  (/* value, id */) => {
-                    /* console.log(value, id); */
-                  }
-                }
-              />
-            </div>
-          </Modal>
-        )}
-      </div>
-    );
-  }
-}
diff --git a/src/components/modal/modal-actions.js b/src/components/modal/modal-actions.js
new file mode 100644
index 0000000..38e1ac0
--- /dev/null
+++ b/src/components/modal/modal-actions.js
@@ -0,0 +1,90 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Button from '../button';
+
+export default class ModalActions extends React.Component {
+  renderSecondary() {
+    const { props } = this;
+    if (!props.primaryAction || !props.secondaryAction) {
+      return null;
+    }
+
+    return (
+      <div className="flex-child flex-child--no-shrink mr12">
+        <Button
+          variant="discouraging"
+          size="medium"
+          onClick={props.secondaryAction.callback}
+          data-test="secondary-modal-action"
+        >
+          {props.secondaryAction.text}
+        </Button>
+      </div>
+    );
+  }
+
+  renderTertiary() {
+    const { props } = this;
+    if (
+      !props.primaryAction ||
+      !props.secondaryAction ||
+      !props.tertiaryAction
+    ) {
+      return null;
+    }
+
+    return (
+      <div className="flex-child flex-child--grow flex-child--no-shrink mr12">
+        <button
+          type="button"
+          className="link link--gray txt-bold txt-s"
+          onClick={props.tertiaryAction.callback}
+          data-test="tertiary-modal-action"
+        >
+          {props.tertiaryAction.text}
+        </button>
+      </div>
+    );
+  }
+
+  render() {
+    const { props } = this;
+
+    const primaryButtonVariant = props.primaryAction.destructive
+      ? 'destructive'
+      : 'primary';
+
+    return (
+      <div className="flex-parent flex-parent--center-main flex-parent--center-cross flex-parent--row-reverse flex-parent--wrap">
+        <div className="flex-child flex-child--no-shrink">
+          <Button
+            variant={primaryButtonVariant}
+            size="medium"
+            onClick={props.primaryAction.callback}
+            data-test="primary-modal-action"
+          >
+            {props.primaryAction.text}
+          </Button>
+        </div>
+        {this.renderSecondary()}
+        {this.renderTertiary()}
+      </div>
+    );
+  }
+}
+
+ModalActions.propTypes = {
+  primaryAction: PropTypes.shape({
+    text: PropTypes.string.isRequired,
+    callback: PropTypes.func.isRequired,
+    destructive: PropTypes.bool
+  }).isRequired,
+  secondaryAction: PropTypes.shape({
+    text: PropTypes.string.isRequired,
+    callback: PropTypes.func.isRequired
+  }),
+  tertiaryAction: PropTypes.shape({
+    text: PropTypes.string.isRequired,
+    callback: PropTypes.func.isRequired
+  })
+};
diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js
index 1a73551..328dd32 100644
--- a/src/components/modal/modal.js
+++ b/src/components/modal/modal.js
@@ -1,126 +1,130 @@
 import React from 'react';
+import classnames from 'classnames';
 import PropTypes from 'prop-types';
 import AriaModal from 'react-aria-modal';
 import IconButton from '../icon-button';
+import getWindow from '../utils/get-window';
+import ModalActions from './modal-actions';
 
+/**
+ * An accessible modal dialog.
+ *
+ * To get a standard button arrangement at the bottom of the modal, use the
+ * `primaryAction`, `secondaryAction`, and `tertiaryAction` props.
+ *
+ * This modal *traps focus within it*. You should be aware of that, because it
+ * can sometimes introduce a hurdle when integrating the modal with other
+ * things, especially third-party libraries. But it's an essential UX feature.
+ */
 export default class Modal extends React.Component {
-  static propTypes = {
-    /** A screen-reader-friendly modal title. Required for accessibility. */
-    title: PropTypes.string.isRequired,
-    /**
-     * Invoked when the modal should close. When provided, a close button is
-     * present in the top right corner. If not provided, the `esc` key and any
-     * outside clicks will not close the modal.
-     */
-    onExit: PropTypes.func,
-    /**
-     * When invoked, it returns the primary application node, which should be
-     * `aria-hidden` when the modal is open. By default, returns
-     * `document.getElementById('app')`.
-     */
-    getApplicationNode: PropTypes.func,
-    /**
-     * Modal container size. Options are `small`, `large`, or `auto`. If `auto`
-     * is provided, a width is not specified.
-     */
-    size: PropTypes.oneOf(['small', 'large', 'auto']),
-    /**
-     * Selector for a specific element that should receive initial focus. The
-     * value will be passed to `querySelector`.
-     */
-    initialFocus: PropTypes.string,
-    /** Component's children, which will be the modal content. */
-    children: PropTypes.node.isRequired,
-    /** Additional style attributes to apply to the underlay container. */
-    underlayStyle: PropTypes.object,
-    /** Additional properties to apply to the underlay container. */
-    underlayProps: PropTypes.object,
-    /** CSS styling classes to apply to the underlay container. */
-    themeUnderlay: PropTypes.string,
-    /** CSS styling classes to apply to the modal container. */
-    themeModal: PropTypes.string,
-    /** CSS styling classes to apply to the close button. */
-    themeButtonClose: PropTypes.string
-  };
-
-  static defaultProps = {
-    size: 'large',
-    getApplicationNode: () => document.getElementById('app'),
-    underlayProps: { 'data-popover-ignore-clicks': true },
-    underlayStyle: {},
-    themeUnderlay: 'bg-darken50 px12 py12 px60-mm py60-mm',
-    themeModal: 'bg-white px36 py36 round',
-    themeButtonClose:
-      'btn btn--transparent unround-t unround-br color-gray py12 px12'
-  };
+  constructor(props) {
+    super(props);
+
+    this.setDialogEl = this.setDialogEl.bind(this);
+  }
+
+  componentDidMount() {
+    // After the modal has mounted, it will receive focus (unless initialFocus)
+    // is set. If it is taller than the viewport, the browser will scroll down
+    // so the top of the modal is at the top of the viewport, instead of
+    // staying at the top where you can see the nice padding. To show the
+    // padding, we get the underlay element and scroll to the top of that
+    // right after the component mounts.
+    if (!this.dialogEl || typeof window === 'undefined') return;
+    // A slight delay is necessary so this happens after focus is placed.
+    this.scrollTimeout = getWindow().setTimeout(() => {
+      const offsetParent = this.dialogEl.offsetParent;
+      if (offsetParent.tagName === 'BODY' || offsetParent.tagName === 'HTML') {
+        return;
+      }
+      offsetParent.scrollTo(0, 0);
+    }, 0);
+  }
+
+  componentWillUnmount() {
+    if (typeof window === 'undefined') return;
+    getWindow().clearTimeout(this.scrollTimeout);
+  }
+
+  setDialogEl(el) {
+    this.dialogEl = el;
+  }
+
+  renderActions() {
+    const { primaryAction, secondaryAction, tertiaryAction } = this.props;
+    if (!primaryAction) {
+      return null;
+    }
+
+    return (
+      <div className="mt24">
+        <ModalActions
+          primaryAction={primaryAction}
+          secondaryAction={secondaryAction}
+          tertiaryAction={tertiaryAction}
+        />
+      </div>
+    );
+  }
 
   render() {
-    const {
-      title,
-      onExit,
-      size,
-      initialFocus,
-      underlayProps,
-      underlayStyle,
-      themeUnderlay,
-      themeModal,
-      themeButtonClose
-    } = this.props;
+    const { props } = this;
 
     let closeButton = null;
-    if (onExit) {
+    if (props.onExit) {
       closeButton = (
         <div className="absolute top right">
           <IconButton
             icon="close"
-            onClick={onExit}
+            onClick={props.onExit}
             tooltipProps={{
               content: 'Close'
             }}
-            themeButton={themeButtonClose}
+            themeButton="btn btn--transparent unround-t unround-br color-gray py12 px12"
           />
         </div>
       );
     }
 
     let widthClass = '';
-    if (size === 'small') {
+    if (props.size === 'small') {
       widthClass = 'w360';
-    }
-
-    if (size === 'large') {
+    } else if (props.size === 'large') {
       widthClass = 'w600';
     }
 
-    const containerClasses = `relative wmax-full ${widthClass}`;
+    const containerClasses = classnames(
+      `relative wmax-full ${widthClass} bg-white round`,
+      {
+        'px36 py36': props.padded
+      }
+    );
 
     const dialogBody = (
-      <div className={`${containerClasses} ${themeModal}`}>
-        {this.props.children}
+      <div ref={this.setDialogEl} className={containerClasses}>
+        {props.children}
+        {this.renderActions()}
         {closeButton}
       </div>
     );
 
-    if (underlayStyle.zIndex === undefined) {
-      underlayStyle.zIndex = 1;
-    }
-
     const modalProps = {
-      titleText: title,
-      getApplicationNode: this.props.getApplicationNode,
-      underlayClass: themeUnderlay,
-      dialogStyle: { boxShadow: 'none' },
-      underlayColor: '',
-      underlayProps,
-      underlayStyle
+      titleText: props.accessibleTitle,
+      getApplicationNode: props.getApplicationNode,
+      underlayProps: { 'data-popover-ignore-clicks': true },
+      underlayClass: 'bg-darken50 px12 py12 px60-mm py60-mm ',
+      underlayStyle: {
+        zIndex: 1
+      },
+      alert: props.alert
     };
 
-    if (onExit) {
-      modalProps.onExit = onExit;
+    if (props.onExit) {
+      modalProps.onExit = props.onExit;
     }
 
-    if (initialFocus) {
-      modalProps.initialFocus = initialFocus;
+    if (props.initialFocus) {
+      modalProps.initialFocus = props.initialFocus;
     } else {
       modalProps.focusDialog = true;
     }
@@ -128,3 +132,110 @@ export default class Modal extends React.Component {
     return <AriaModal {...modalProps}>{dialogBody}</AriaModal>;
   }
 }
+
+Modal.propTypes = {
+  /**
+   * A screen-reader-friendly modal title. Required for accessibility.
+   *
+   * This **will not be displayed.** It's only for screen readers.
+   * You can visually display your own header text however you'd like.
+   */
+  accessibleTitle: PropTypes.string.isRequired,
+  /**
+   * Invoked when the modal should close. When this callback is provided,
+   * a close button will be in the top right corner, and a click on the underlay
+   * or the Escape key will close the modal.
+   *
+   * If this prop is not provided, the close button will not be present and
+   * a click on the underlay or Escape will not close the modal. The reason
+   * you might not provide this function is that you want to force the user
+   * to do something, instead of allowing them to sneak out of the modal.
+   */
+  onExit: PropTypes.func,
+  /**
+   * A function that should the primary application node, which should be
+   * `aria-hidden` when the modal is open. By default, returns
+   * `document.getElementById('app')`.
+   */
+  getApplicationNode: PropTypes.func,
+  /**
+   * Modal container size. Options are `small`, `large`, or `auto`. If `auto`
+   * is provided, a width is not specified.
+   */
+  size: PropTypes.oneOf(['small', 'large', 'auto']),
+  /**
+   * Selector for a specific element that should receive initial focus. The
+   * value will be passed to `querySelector`.
+   */
+  initialFocus: PropTypes.string,
+  /**
+   * The content of the modal.
+   */
+  children: PropTypes.node.isRequired,
+  /**
+   * If `false`, the modal will not have its default padding.
+   */
+  padded: PropTypes.bool,
+  /**
+   * If `true`, the modal will have the accessibility props of an alert modal.
+   * (Only affects screen readers.)
+   */
+  alert: PropTypes.bool,
+  /**
+   * The modal's primary action. If this is provided, an encouraging
+   * button will be rendered at the bottom of the modal.
+   *
+   * Provide this and other action props if you want a standard button
+   * arrangement at the bottom of the modal. If you need a more custom
+   * arrangement, leave them out and insert your buttons into the content.
+   *
+   * The value is an object with the following properties:
+   * - `text`: **(required)** The text of the button.
+   * - `callback`: **(required)** Invoked when the button is clicked.
+   * - `destructive`: If `true`, the [Button](#button) will be primed for
+   *   desctruction.
+   */
+  primaryAction: PropTypes.shape({
+    text: PropTypes.string.isRequired,
+    callback: PropTypes.func.isRequired,
+    destructive: PropTypes.bool
+  }),
+  /**
+   * The modal's secondary action. If this is provided, a discouraging button
+   * will be rendered at the bottom of the modal. See the description of
+   * `primaryAction`.
+   *
+   * **Can only be used in combination with `primaryAction`.**
+   *
+   * The value is an object with the following properties:
+   * - `text`: **(required)** The text of the button.
+   * - `callback`: **(required)** Invoked when the button is clicked.
+   */
+  secondaryAction: PropTypes.shape({
+    text: PropTypes.string.isRequired,
+    callback: PropTypes.func.isRequired
+  }),
+  /**
+   * The modal's tertiary action. **You should rarely if ever need this.**
+   * If this is provided, a *very* discouraging button
+   * will be rendered at the bottom of the modal. See the description of
+   * `primaryAction`.
+   *
+   * **Can only be used in combination with `primaryAction` and
+   * `secondaryAction`.**
+   *
+   * The value is an object with the following properties:
+   * - `text`: **(required)** The text of the button.
+   * - `callback`: **(required)** Invoked when the button is clicked.
+   */
+  tertiaryAction: PropTypes.shape({
+    text: PropTypes.string.isRequired,
+    callback: PropTypes.func.isRequired
+  })
+};
+
+Modal.defaultProps = {
+  size: 'large',
+  getApplicationNode: () => document.getElementById('app'),
+  padded: true
+};


Test patch:
diff --git a/src/components/modal/__tests__/__snapshots__/modal-actions.test.js.snap b/src/components/modal/__tests__/__snapshots__/modal-actions.test.js.snap
new file mode 100644
index 0000000..31931c2
--- /dev/null
+++ b/src/components/modal/__tests__/__snapshots__/modal-actions.test.js.snap
@@ -0,0 +1,125 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`primary and secondary renders as expected 1`] = `
+<div
+  className="flex-parent flex-parent--center-main flex-parent--center-cross flex-parent--row-reverse flex-parent--wrap"
+>
+  <div
+    className="flex-child flex-child--no-shrink"
+  >
+    <Button
+      block={false}
+      data-test="primary-modal-action"
+      disabled={false}
+      onClick={[MockFunction]}
+      size="medium"
+      variant="primary"
+    >
+      Okay
+    </Button>
+  </div>
+  <div
+    className="flex-child flex-child--no-shrink mr12"
+  >
+    <Button
+      block={false}
+      data-test="secondary-modal-action"
+      disabled={false}
+      onClick={[MockFunction]}
+      size="medium"
+      variant="discouraging"
+    >
+      Cancel
+    </Button>
+  </div>
+</div>
+`;
+
+exports[`primary and tertiary does not render tertiary button, since secondary is not provided 1`] = `
+<div
+  className="flex-parent flex-parent--center-main flex-parent--center-cross flex-parent--row-reverse flex-parent--wrap"
+>
+  <div
+    className="flex-child flex-child--no-shrink"
+  >
+    <Button
+      block={false}
+      data-test="primary-modal-action"
+      disabled={false}
+      onClick={[MockFunction]}
+      size="medium"
+      variant="primary"
+    >
+      Okay
+    </Button>
+  </div>
+</div>
+`;
+
+exports[`primary only renders as expected 1`] = `
+<div
+  className="flex-parent flex-parent--center-main flex-parent--center-cross flex-parent--row-reverse flex-parent--wrap"
+>
+  <div
+    className="flex-child flex-child--no-shrink"
+  >
+    <Button
+      block={false}
+      data-test="primary-modal-action"
+      disabled={false}
+      onClick={[MockFunction]}
+      size="medium"
+      variant="primary"
+    >
+      Okay
+    </Button>
+  </div>
+</div>
+`;
+
+exports[`primary, secondary, tertiary renders as expected 1`] = `
+<div
+  className="flex-parent flex-parent--center-main flex-parent--center-cross flex-parent--row-reverse flex-parent--wrap"
+>
+  <div
+    className="flex-child flex-child--no-shrink"
+  >
+    <Button
+      block={false}
+      data-test="primary-modal-action"
+      disabled={false}
+      onClick={[MockFunction]}
+      size="medium"
+      variant="primary"
+    >
+      Okay
+    </Button>
+  </div>
+  <div
+    className="flex-child flex-child--no-shrink mr12"
+  >
+    <Button
+      block={false}
+      data-test="secondary-modal-action"
+      disabled={false}
+      onClick={[MockFunction]}
+      size="medium"
+      variant="discouraging"
+    >
+      Cancel
+    </Button>
+  </div>
+  <div
+    className="flex-child flex-child--grow flex-child--no-shrink mr12"
+  >
+    <button
+      className="link link--gray txt-bold txt-s"
+      data-test="tertiary-modal-action"
+      onClick={[MockFunction]}
+      type="button"
+    >
+      Give up
+    </button>
+  </div>
+</div>
+`;
diff --git a/src/components/modal/__tests__/__snapshots__/modal.test.js.snap b/src/components/modal/__tests__/__snapshots__/modal.test.js.snap
index 39ed033..a752129 100644
--- a/src/components/modal/__tests__/__snapshots__/modal.test.js.snap
+++ b/src/components/modal/__tests__/__snapshots__/modal.test.js.snap
@@ -2,18 +2,12 @@
 
 exports[`Modal No fixed width, display only renders 1`] = `
 <Displaced
-  dialogStyle={
-    Object {
-      "boxShadow": "none",
-    }
-  }
   focusDialog={true}
   getApplicationNode={[Function]}
   mounted={true}
   onExit={[MockFunction]}
   titleText="Auto modal title"
-  underlayClass="bg-darken50 px12 py12 px60-mm py60-mm"
-  underlayColor=""
+  underlayClass="bg-darken50 px12 py12 px60-mm py60-mm "
   underlayProps={
     Object {
       "data-popover-ignore-clicks": true,
@@ -26,10 +20,10 @@ exports[`Modal No fixed width, display only renders 1`] = `
   }
 >
   <div
-    className="relative wmax-full  bg-white px36 py36 round"
+    className="relative wmax-full  bg-white round px36 py36"
   >
     <div>
-      No fixed width on the modal container
+      No fixed width on the modal container. But very tall.
     </div>
     <div
       className="absolute top right"
@@ -51,32 +45,25 @@ exports[`Modal No fixed width, display only renders 1`] = `
 
 exports[`Modal all options renders 1`] = `
 <Displaced
-  dialogStyle={
-    Object {
-      "boxShadow": "none",
-    }
-  }
   getApplicationNode={[Function]}
   initialFocus="#foo"
   mounted={true}
   onExit={[MockFunction]}
   titleText="All options"
-  underlayClass="bg-pink px24 py24"
-  underlayColor=""
+  underlayClass="bg-darken50 px12 py12 px60-mm py60-mm "
   underlayProps={
     Object {
-      "data-foo": "foo",
+      "data-popover-ignore-clicks": true,
     }
   }
   underlayStyle={
     Object {
-      "margin": "pink",
       "zIndex": 1,
     }
   }
 >
   <div
-    className="relative wmax-full w360 bg-gray-light round py60 px60"
+    className="relative wmax-full w360 bg-white round"
   >
     <div>
       <div
@@ -111,18 +98,12 @@ exports[`Modal all options renders 1`] = `
 
 exports[`Modal basic default renders 1`] = `
 <Displaced
-  dialogStyle={
-    Object {
-      "boxShadow": "none",
-    }
-  }
   focusDialog={true}
   getApplicationNode={[Function]}
   mounted={true}
   onExit={[MockFunction]}
   titleText="Large modal title"
-  underlayClass="bg-darken50 px12 py12 px60-mm py60-mm"
-  underlayColor=""
+  underlayClass="bg-darken50 px12 py12 px60-mm py60-mm "
   underlayProps={
     Object {
       "data-popover-ignore-clicks": true,
@@ -135,11 +116,36 @@ exports[`Modal basic default renders 1`] = `
   }
 >
   <div
-    className="relative wmax-full w600 bg-white px36 py36 round"
+    className="relative wmax-full w600 bg-white round px36 py36"
   >
     <div>
       Large modal body
     </div>
+    <div
+      className="mt24"
+    >
+      <ModalActions
+        primaryAction={
+          Object {
+            "callback": [MockFunction],
+            "destructive": true,
+            "text": "Okay",
+          }
+        }
+        secondaryAction={
+          Object {
+            "callback": [MockFunction],
+            "text": "Cancel",
+          }
+        }
+        tertiaryAction={
+          Object {
+            "callback": [MockFunction],
+            "text": "Give up",
+          }
+        }
+      />
+    </div>
     <div
       className="absolute top right"
     >
@@ -160,18 +166,12 @@ exports[`Modal basic default renders 1`] = `
 
 exports[`Modal basic small renders 1`] = `
 <Displaced
-  dialogStyle={
-    Object {
-      "boxShadow": "none",
-    }
-  }
   focusDialog={true}
   getApplicationNode={[Function]}
   mounted={true}
   onExit={[MockFunction]}
   titleText="Small modal title"
-  underlayClass="bg-darken50 px12 py12 px60-mm py60-mm"
-  underlayColor=""
+  underlayClass="bg-darken50 px12 py12 px60-mm py60-mm "
   underlayProps={
     Object {
       "data-popover-ignore-clicks": true,
@@ -184,11 +184,23 @@ exports[`Modal basic small renders 1`] = `
   }
 >
   <div
-    className="relative wmax-full w360 bg-white px36 py36 round"
+    className="relative wmax-full w360 bg-white round px36 py36"
   >
     <div>
       Small modal body
     </div>
+    <div
+      className="mt24"
+    >
+      <ModalActions
+        primaryAction={
+          Object {
+            "callback": [MockFunction],
+            "text": "Okay",
+          }
+        }
+      />
+    </div>
     <div
       className="absolute top right"
     >
@@ -209,17 +221,11 @@ exports[`Modal basic small renders 1`] = `
 
 exports[`Modal no onExit renders 1`] = `
 <Displaced
-  dialogStyle={
-    Object {
-      "boxShadow": "none",
-    }
-  }
   focusDialog={true}
   getApplicationNode={[Function]}
   mounted={true}
   titleText="No onExit passed as prop"
-  underlayClass="bg-darken50 px12 py12 px60-mm py60-mm"
-  underlayColor=""
+  underlayClass="bg-darken50 px12 py12 px60-mm py60-mm "
   underlayProps={
     Object {
       "data-popover-ignore-clicks": true,
@@ -232,7 +238,7 @@ exports[`Modal no onExit renders 1`] = `
   }
 >
   <div
-    className="relative wmax-full w360 bg-white px36 py36 round"
+    className="relative wmax-full w360 bg-white round px36 py36"
   >
     <div>
       You can't close me
diff --git a/src/components/modal/__tests__/modal-actions.test.js b/src/components/modal/__tests__/modal-actions.test.js
new file mode 100644
index 0000000..d2ffdbd
--- /dev/null
+++ b/src/components/modal/__tests__/modal-actions.test.js
@@ -0,0 +1,117 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import ModalActions from '../modal-actions';
+
+describe('primary only', () => {
+  let wrapper;
+  let primaryCallback;
+
+  beforeEach(() => {
+    primaryCallback = jest.fn();
+    wrapper = shallow(
+      <ModalActions
+        primaryAction={{ text: 'Okay', callback: primaryCallback }}
+      />
+    );
+  });
+
+  test('renders as expected', () => {
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  test('fires primary callback', () => {
+    wrapper.find({ 'data-test': 'primary-modal-action' }).prop('onClick')();
+    expect(primaryCallback).toHaveBeenCalledTimes(1);
+  });
+});
+
+describe('primary and secondary', () => {
+  let wrapper;
+  let primaryCallback;
+  let secondaryCallback;
+
+  beforeEach(() => {
+    primaryCallback = jest.fn();
+    secondaryCallback = jest.fn();
+    wrapper = shallow(
+      <ModalActions
+        primaryAction={{ text: 'Okay', callback: primaryCallback }}
+        secondaryAction={{ text: 'Cancel', callback: secondaryCallback }}
+      />
+    );
+  });
+
+  test('renders as expected', () => {
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  test('fires primary callback', () => {
+    wrapper.find({ 'data-test': 'primary-modal-action' }).prop('onClick')();
+    expect(primaryCallback).toHaveBeenCalledTimes(1);
+  });
+
+  test('fires secondary callback', () => {
+    wrapper.find({ 'data-test': 'secondary-modal-action' }).prop('onClick')();
+    expect(secondaryCallback).toHaveBeenCalledTimes(1);
+  });
+});
+
+describe('primary, secondary, tertiary', () => {
+  let wrapper;
+  let primaryCallback;
+  let secondaryCallback;
+  let tertiaryCallback;
+
+  beforeEach(() => {
+    primaryCallback = jest.fn();
+    secondaryCallback = jest.fn();
+    tertiaryCallback = jest.fn();
+    wrapper = shallow(
+      <ModalActions
+        primaryAction={{ text: 'Okay', callback: primaryCallback }}
+        secondaryAction={{ text: 'Cancel', callback: secondaryCallback }}
+        tertiaryAction={{ text: 'Give up', callback: tertiaryCallback }}
+      />
+    );
+  });
+
+  test('renders as expected', () => {
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  test('fires primary callback', () => {
+    wrapper.find({ 'data-test': 'primary-modal-action' }).prop('onClick')();
+    expect(primaryCallback).toHaveBeenCalledTimes(1);
+  });
+
+  test('fires secondary callback', () => {
+    wrapper.find({ 'data-test': 'secondary-modal-action' }).prop('onClick')();
+    expect(secondaryCallback).toHaveBeenCalledTimes(1);
+  });
+
+  test('fires tertiary callback', () => {
+    wrapper.find({ 'data-test': 'tertiary-modal-action' }).prop('onClick')();
+    expect(tertiaryCallback).toHaveBeenCalledTimes(1);
+  });
+});
+
+describe('primary and tertiary', () => {
+  let wrapper;
+  let primaryCallback;
+  let tertiaryCallback;
+
+  beforeEach(() => {
+    primaryCallback = jest.fn();
+    tertiaryCallback = jest.fn();
+    wrapper = shallow(
+      <ModalActions
+        primaryAction={{ text: 'Okay', callback: primaryCallback }}
+        tertiaryAction={{ text: 'Cancel', callback: tertiaryCallback }}
+      />
+    );
+  });
+
+  test('does not render tertiary button, since secondary is not provided', () => {
+    expect(wrapper).toMatchSnapshot();
+  });
+});
diff --git a/src/components/modal/__tests__/modal-test-cases.js b/src/components/modal/__tests__/modal-test-cases.js
index 08f1235..a4c76c5 100644
--- a/src/components/modal/__tests__/modal-test-cases.js
+++ b/src/components/modal/__tests__/modal-test-cases.js
@@ -16,7 +16,7 @@ class ModalWrapper extends React.Component {
   };
 
   toggleModal = () => {
-    this.setState({ modalOpen: !this.state.modalOpen });
+    this.setState(state => ({ modalOpen: !state.modalOpen }));
   };
 
   render() {
@@ -39,9 +39,13 @@ testCases.basicSmallDisplay = {
   element: (
     <ModalWrapper
       componentProps={{
-        title: 'Small modal title',
+        accessibleTitle: 'Small modal title',
         size: 'small',
-        children: <div>Small modal body</div>
+        children: <div>Small modal body</div>,
+        primaryAction: {
+          text: 'Okay',
+          callback: safeSpy()
+        }
       }}
     />
   )
@@ -52,8 +56,21 @@ testCases.basicLargeDisplay = {
   element: (
     <ModalWrapper
       componentProps={{
-        title: 'Large modal title',
-        children: <div>Large modal body</div>
+        accessibleTitle: 'Large modal title',
+        children: <div>Large modal body</div>,
+        primaryAction: {
+          text: 'Okay',
+          callback: safeSpy(),
+          destructive: true
+        },
+        secondaryAction: {
+          text: 'Cancel',
+          callback: safeSpy()
+        },
+        tertiaryAction: {
+          text: 'Give up',
+          callback: safeSpy()
+        }
       }}
     />
   )
@@ -64,29 +81,33 @@ testCases.basicAutoDisplay = {
   element: (
     <ModalWrapper
       componentProps={{
-        title: 'Auto modal title',
+        accessibleTitle: 'Auto modal title',
         size: 'auto',
-        children: <div>No fixed width on the modal container</div>
+        children: (
+          <div style={{ height: 1200 }}>
+            No fixed width on the modal container
+          </div>
+        )
       }}
     />
   )
 };
 
-testCases.themed = {
-  description: 'themed modal',
+testCases.unpadded = {
+  description: 'unpadded modal',
   element: (
     <ModalWrapper
       componentProps={{
-        title: 'Large modal title',
-        children: <div>Theme modal container and close button</div>,
-        themeModal: 'bg-gray round py60 px60 color-white',
-        themeButtonClose:
-          'btn btn--transparent unround-t unround-br color-white py12 px12',
-        underlayStyle: {
-          transform: 'scale(0.9)',
-          zIndex: 5
-        },
-        themeUnderlay: 'bg-pink px24 py24'
+        accessibleTitle: 'Unpadded',
+        children: (
+          <div className="flex-parent flex-parent--stretch-cross">
+            <div className="flex-child w300 bg-gray-dark px24 py24 round-l color-white">
+              One side
+            </div>
+            <div className="flex-child px24 py24">The other side</div>
+          </div>
+        ),
+        padded: false
       }}
     />
   )
@@ -94,59 +115,69 @@ testCases.themed = {
 
 // Automatable test cases
 
-testCases.basicLarge = {
+const noDisplayCases = {};
+
+noDisplayCases.basicLarge = {
   description: 'basic default',
   component: Modal,
   props: {
-    title: 'Large modal title',
+    accessibleTitle: 'Large modal title',
     onExit: safeSpy(),
-    children: <div>Large modal body</div>
+    children: <div>Large modal body</div>,
+    primaryAction: {
+      text: 'Okay',
+      callback: safeSpy(),
+      destructive: true
+    },
+    secondaryAction: {
+      text: 'Cancel',
+      callback: safeSpy()
+    },
+    tertiaryAction: {
+      text: 'Give up',
+      callback: safeSpy()
+    }
   },
   noDisplay: true
 };
 
-testCases.basicSmall = {
+noDisplayCases.basicSmall = {
   description: 'basic small',
   component: Modal,
   props: {
-    title: 'Small modal title',
+    accessibleTitle: 'Small modal title',
     onExit: safeSpy(),
     size: 'small',
-    children: <div>Small modal body</div>
+    children: <div>Small modal body</div>,
+    primaryAction: {
+      text: 'Okay',
+      callback: safeSpy()
+    }
   },
   noDisplay: true
 };
 
-testCases.basicAuto = {
+noDisplayCases.basicAuto = {
   description: 'No fixed width, display only',
   component: Modal,
   props: {
-    title: 'Auto modal title',
+    accessibleTitle: 'Auto modal title',
     onExit: safeSpy(),
     size: 'auto',
-    children: <div>No fixed width on the modal container</div>
+    children: <div>No fixed width on the modal container. But very tall.</div>
   },
   noDisplay: true
 };
 
-testCases.allOptions = {
+noDisplayCases.allOptions = {
   description: 'all options',
   component: Modal,
   props: {
-    title: 'All options',
+    accessibleTitle: 'All options',
     size: 'small',
     onExit: safeSpy(),
     initialFocus: '#foo',
-    themeModal: 'bg-gray-light round py60 px60',
-    themeButtonClose:
-      'btn btn--transparent unround-t unround-br color-gray py12 px12',
-    underlayProps: {
-      'data-foo': 'foo'
-    },
-    underlayStyle: {
-      margin: 'pink'
-    },
-    themeUnderlay: 'bg-pink px24 py24',
+    padded: false,
     children: (
       <div>
         <div className="mb12">I am a message</div>
@@ -159,15 +190,15 @@ testCases.allOptions = {
   noDisplay: true
 };
 
-testCases.optionalOnExit = {
+noDisplayCases.optionalOnExit = {
   description: 'no onExit',
   component: Modal,
   props: {
-    title: 'No onExit passed as prop',
+    accessibleTitle: 'No onExit passed as prop',
     size: 'small',
     children: <div>You can't close me</div>
   },
   noDisplay: true
 };
 
-export { testCases };
+export { testCases, noDisplayCases };
diff --git a/src/components/modal/__tests__/modal.test.js b/src/components/modal/__tests__/modal.test.js
index d6b6d4d..f482510 100644
--- a/src/components/modal/__tests__/modal.test.js
+++ b/src/components/modal/__tests__/modal.test.js
@@ -1,14 +1,14 @@
 import React from 'react';
 import { shallow } from 'enzyme';
-import { testCases } from './modal-test-cases';
+import { noDisplayCases } from './modal-test-cases';
 
 describe('Modal', () => {
   let testCase;
   let wrapper;
 
-  describe(testCases.basicSmall.description, () => {
+  describe(noDisplayCases.basicSmall.description, () => {
     beforeEach(() => {
-      testCase = testCases.basicSmall;
+      testCase = noDisplayCases.basicSmall;
       wrapper = shallow(
         React.createElement(testCase.component, testCase.props)
       );
@@ -26,9 +26,9 @@ describe('Modal', () => {
     });
   });
 
-  describe(testCases.basicLarge.description, () => {
+  describe(noDisplayCases.basicLarge.description, () => {
     beforeEach(() => {
-      testCase = testCases.basicLarge;
+      testCase = noDisplayCases.basicLarge;
       wrapper = shallow(
         React.createElement(testCase.component, testCase.props)
       );
@@ -46,9 +46,9 @@ describe('Modal', () => {
     });
   });
 
-  describe(testCases.basicAuto.description, () => {
+  describe(noDisplayCases.basicAuto.description, () => {
     beforeEach(() => {
-      testCase = testCases.basicAuto;
+      testCase = noDisplayCases.basicAuto;
       wrapper = shallow(
         React.createElement(testCase.component, testCase.props)
       );
@@ -66,9 +66,9 @@ describe('Modal', () => {
     });
   });
 
-  describe(testCases.allOptions.description, () => {
+  describe(noDisplayCases.allOptions.description, () => {
     beforeEach(() => {
-      testCase = testCases.allOptions;
+      testCase = noDisplayCases.allOptions;
       wrapper = shallow(
         React.createElement(testCase.component, testCase.props)
       );
@@ -79,9 +79,9 @@ describe('Modal', () => {
     });
   });
 
-  describe(testCases.optionalOnExit.description, () => {
+  describe(noDisplayCases.optionalOnExit.description, () => {
     beforeEach(() => {
-      testCase = testCases.optionalOnExit;
+      testCase = noDisplayCases.optionalOnExit;
       wrapper = shallow(
         React.createElement(testCase.component, testCase.props)
       );