Repository: speee/jsx-slack
Pull request #60 (2019-09-27 05:29:00)

<Modal> container component
This PR has implemented a part of #57.

`<Modal>` is a new container component to generate [JSON payload for Slack view](https://api.slack.com/reference/block-kit/views). It allows opening / updating modal and dialog that include Block Kit components by passing to Slack API: `views.open` and `views.update`.

Unlike an outdated `<Dialog>`, `<Modal>` would be implemented to main entrypoint `@speee-js/jsx-slack`.

### An example on [Bolt](https://slack.dev/bolt)

```jsx
app.action('modal', ({ ack, body, context }) => {
  ack()

  app.client.views.open({
    token: context.botToken,
    trigger_id: body.trigger_id,
    view: JSXSlack(
      <Modal title="The first modal">
        <Section>
          Hello, modal!
        </Section>
        <Divider />
        <Image src="https://repository-images.githubusercontent.com/170699288/65347f00-64dd-11e9-912a-fadbec4a6771" alt="@speee-js/jsx-slack" />
      </Modal>
    )
  })
})
```

<img width="538" alt="example" src="https://user-images.githubusercontent.com/3993388/65744323-fc5bcd80-e132-11e9-85d9-60c8a63bf614.png">

## ToDo

- [x] Add `<Modal>` component
  - [x] Arguments
- [x] Fill tests
- [x] Soft-deprecate `<Dialog>`
- [x] Update documents


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0cd65a9..9eb33f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,11 +5,16 @@
 ### Added
 
 - [Multi-select menus](https://api.slack.com/reference/block-kit/block-elements#multi_select) ([#56](https://github.com/speee/jsx-slack/issues/56), [#58](https://github.com/speee/jsx-slack/pull/58))
+- `<Modal>` container component ([#60](https://github.com/speee/jsx-slack/pull/60))
 
 ### Changed
 
 - Bump dependent packages to the latest version ([#59](https://github.com/speee/jsx-slack/pull/59))
 
+### Deprecated
+
+- Mark `<Dialog>` as soft-deprecated in favor of Slack Modals ([#60](https://github.com/speee/jsx-slack/pull/60))
+
 ## v0.9.2 - 2019-08-29
 
 ### Fixed
diff --git a/README.md b/README.md
index f751488..082bee0 100644
--- a/README.md
+++ b/README.md
@@ -9,12 +9,11 @@
 [npm]: https://www.npmjs.com/package/@speee-js/jsx-slack
 [license]: ./LICENSE
 
-Build JSON object for [Slack][slack] [Block Kit] and [dialog] from readable [JSX].
+Build JSON object for [Slack][slack] [Block Kit] from readable [JSX].
 
 [slack]: https://slack.com
 [jsx]: https://reactjs.org/docs/introducing-jsx.html
 [block kit]: https://api.slack.com/block-kit
-[dialog]: https://api.slack.com/dialogs
 [block kit builder]: https://api.slack.com/tools/block-kit-builder
 
 <p align="center">
@@ -26,9 +25,8 @@ Build JSON object for [Slack][slack] [Block Kit] and [dialog] from readable [JSX
 
 ### Features
 
-- **[Block Kit as components](docs/jsx-components-for-block-kit.md)** - Build your message by block components.
+- **[Block Kit as components](docs/jsx-components-for-block-kit.md)** - Build your message by Block Kit block components.
 - **[HTML-like formatting](docs/html-like-formatting.md)** - Keep a readability by using well-known elements.
-- **[Build dialog with HTML form style](docs/jsx-components-for-dialog.md)** - Create Dialog JSON through HTML form style JSX.
 
 ## Motivation
 
@@ -40,7 +38,7 @@ Slack has shipped [Block Kit] and [Block Kit Builder], and efforts to develop ap
 
 A project goal is creating an interface to build a maintainable Slack message and interactive contents with confidence via readable [JSX].
 
-jsx-slack would allow building message blocks and dialogs with predictable HTML-like markup. It helps in understanding the structure of complex messages and interactions.
+jsx-slack would allow composing blocks with predictable HTML-like markup. It helps in understanding the structure of complex messages and interactions.
 
 ## Install
 
@@ -165,7 +163,12 @@ Slack has recommended to use **[Block Kit]** for building tempting message. By u
 - **[HTML-like formatting](docs/html-like-formatting.md)**
 - **[About escape and exact mode](docs/about-escape-and-exact-mode.md)**
 
-## Dialog as components
+## Dialog as components _(deprecated)_
+
+> :warning: Dialog components provided by `@speee-js/jsx-slack/dialog` are now deprecated in favor of [Modals](https://api.slack.com/block-kit/surfaces/modals) whose supported Block Kit. You can still use that entry point for the outdated dialog of Slack, but you should migrate into Modals because we would be removed `@speee-js/jsx-slack/dialog` in v1.
+
+<details>
+<p><summary>Show deprecated details...</summary></p>
 
 We also provide `@speee-js/jsx-slack/dialog` to allow building Dialog JSON with same feeling. You can create Dialog JSON for `dialog` argument in [`dialog.open` Slack API](https://api.slack.com/methods/dialog.open), with familiar HTML form style.
 
@@ -191,6 +194,8 @@ export default function exampleDialog(data) {
 
 - **[JSX components for dialog](docs/jsx-components-for-dialog.md)**
 
+</details>
+
 ## Fragments
 
 [As like as React](https://reactjs.org/docs/fragments.html), jsx-slack provides `<Fragment>` (`<JSXSlack.Fragment>`) component for higher-order component (HOC) consited of multiple blocks or elements.
@@ -288,13 +293,7 @@ console.log(jsxslack`
 
 Please notice to a usage of component that has a bit different syntax from JSX.
 
-#### Note about dialog components
-
-`<Select>` and similar components are provided by the both of [Block Kit](docs/jsx-components-for-block-kit.md#select-select-menu-with-static-options) and [Dialog](docs/jsx-components-for-dialog.md#select-select-field-with-static-options).
-
-These within template literal will be resolved by the container component [`<Blocks>`](docs/jsx-components-for-block-kit.md#blocks-1) and [`<Dialog>`](docs/jsx-components-for-dialog.md#dialog-create-dialog-json) smartly, but the most use cases of `jsxslack.fragment` won't define the container so jsx-slack prefers [Block Kit components](docs/jsx-components-for-block-kit.md#select-select-menu-with-static-options).
-
-You can use [dialog components](docs/jsx-components-for-dialog.md#select-select-field-with-static-options) correctly by adding prefix `Dialog.XXXXXX`. (e.g. [`<Dialog.Select>`](docs/jsx-components-for-dialog.md#select-select-field-with-static-options))
+> In case using _deprecated_ dialog components on the out of `<Dialog>` within template literal, you have to use `Dialog.` prefix such as `<Dialog.Select>`.
 
 ## Similar projects
 
diff --git a/demo/example.js b/demo/example.js
index a92eaa9..50d3544 100644
--- a/demo/example.js
+++ b/demo/example.js
@@ -26,11 +26,11 @@ export const blockKit = `
 `.trim()
 
 export const dialog = `
+<!-- ⚠️ NOTE: Please notice outdated Dialog components were deprecated. -->
 <Dialog callbackId="createUser" title="Create user">
   <Input name="name" label="Name" required />
   <Textarea name="desc" label="Description" maxLength="300" />
 
-  <!-- NOTE: Unprefixed Select also would work in Dialog container. -->
   <Dialog.Select name="role" label="Role" value="regular" required>
     <Option value="regular">Regular</Option>
     <Option value="leader">Leader</Option>
diff --git a/demo/index.html b/demo/index.html
index 6fb9b82..37ce231 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -5,11 +5,11 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <meta http-equiv="X-UA-Compatible" content="ie=edge" />
     <title>
-      @speee-js/jsx-slack: Build JSON for Slack Block Kit and dialog from JSX
+      @speee-js/jsx-slack: Build JSON for Slack Block Kit from JSX
     </title>
     <meta
       name="Description"
-      content="Build JSON object for Slack Block Kit and dialog from readable JSX"
+      content="Build JSON object for Slack Block Kit from readable JSX"
     />
     <link rel="stylesheet" type="text/css" href="index.css" />
   </head>
@@ -23,7 +23,7 @@
           >@speee-js/jsx-slack</a
         >
         <small>
-          Build JSON for Slack Block Kit and dialog from JSX
+          Build JSON for Slack Block Kit from JSX
         </small>
       </h1>
       <aside>
@@ -57,7 +57,7 @@
           <select id="examples">
             <option value="" selected>Choose examples...</option>
             <option value="blockKit">Block Kit (default)</option>
-            <option value="dialog">Dialog</option>
+            <option value="dialog">Dialog (DEPRECATED)</option>
           </select>
         </h2>
         <div id="jsx"></div>
diff --git a/docs/jsx-components-for-block-kit.md b/docs/jsx-components-for-block-kit.md
index 2c27c03..25153cc 100644
--- a/docs/jsx-components-for-block-kit.md
+++ b/docs/jsx-components-for-block-kit.md
@@ -1,10 +1,61 @@
 # JSX components for Block Kit
 
-## Blocks
+## Block containers
 
-### `<Blocks>`
+Slack provides [multiple surfaces to place Block Kit blocks](https://api.slack.com/block-kit/surfaces). So you should choose the parent container component depending on purpose.
 
-A container component to use Block Kit. You should wrap Block Kit elements by `<Blocks>`.
+### `<Blocks>`: The basic container for blocks
+
+A basic container component for [Block Kit in messaging](https://api.slack.com/block-kit/surfaces/messages). Wrap layout block components in `<Blocks>`.
+
+When composing message for using in API such as [`chat.postMessage`](https://api.slack.com/methods/chat.postMessage), you should pass generated array by this container component to `blocks` field in payloads.
+
+```javascript
+import { WebClient } from '@slack/client'
+import JSXSlack, { Blocks, Section } from '@speee-js/jsx-slack'
+
+const api = new WebClient(process.env.SLACK_TOKEN)
+
+api.chat.postMessage({
+  channel: 'C1232456',
+  blocks: JSXSlack(
+    <Blocks>
+      <Section>Hello, world!</Section>
+    </Blocks>
+  ),
+})
+```
+
+### `<Modal>`: The modal view container
+
+A container component for [Block Kit in modals](https://api.slack.com/block-kit/surfaces/modals). For focusing into interactions between user and app, you can create JSON object for modal powered by Block Kit.
+
+A generated object by `<Modal>` container should pass to a `view` field in [`views.*`](https://api.slack.com/methods/views.open) API payloads.
+
+```javascript
+api.views.open({
+  // NOTE: trigger_id received from another interaction is required.
+  trigger_id: 'xxxxx.xxxxx.xxxxxxxxxxxx',
+  view: JSXSlack(
+    <Modal title="My first modal">
+      <Section>Hello, modal!</Section>
+    </Modal>
+  ),
+})
+```
+
+#### Props
+
+- `title` (**required**): An user-facing title of the modal. (24 characters maximum)
+- `close` (optional): A text for close button of the modal. (24 characters maximum)
+- `submit` (optional): A text for submit button of the modal. (24 characters maximum)
+- `privateMetadata` (optional): An optional string that can be found in payloads of some interactive events Slack app received. (3000 characters maximum)
+- `clearOnClose` (optional): If enabled by setting `true`, all stacked views will be cleared by close button.
+- `notifyOnClose` (optional): If enabled by setting `true`, `view_closed` event will be sent to request URL of Slack app when closed modal.
+- `callbackId` (optional): An identifier for this modal to recognize it in various events. (255 characters maximum)
+- `externalId` (optional): A unique ID for all views on a per-team basis.
+
+## Layout blocks
 
 ### [`<Section>`: Section Block](https://api.slack.com/reference/messaging/blocks#section)
 
diff --git a/docs/jsx-components-for-dialog.md b/docs/jsx-components-for-dialog.md
index 500bdc8..847ab13 100644
--- a/docs/jsx-components-for-dialog.md
+++ b/docs/jsx-components-for-dialog.md
@@ -1,4 +1,6 @@
-# JSX components for dialog
+# JSX components for dialog _(deprecated)_
+
+> :warning: Dialog components are now deprecated in favor of [Modals](https://api.slack.com/block-kit/surfaces/modals) whose supported Block Kit. You can still use that entry point for the outdated dialog of Slack, but you should migrate into Modals because these are removed in v1.
 
 Dialog components provide from another entry point **`@speee-js/jsx-slack/dialog`** because the specification is different from Block Kit.
 
diff --git a/package.json b/package.json
index 6d7b8b7..57fce26 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@speee-js/jsx-slack",
   "version": "0.9.2",
-  "description": "Build JSON object for Slack Block Kit and dialog from readable JSX",
+  "description": "Build JSON object for Slack Block Kit from readable JSX",
   "author": {
     "name": "Yuki Hattori",
     "url": "https://github.com/yhatt"
diff --git a/src/block-kit/Image.tsx b/src/block-kit/Image.tsx
index 6cc55d4..254cbf1 100644
--- a/src/block-kit/Image.tsx
+++ b/src/block-kit/Image.tsx
@@ -2,6 +2,7 @@
 import { ImageBlock } from '@slack/types'
 import { JSXSlack } from '../jsx'
 import { ObjectOutput } from '../utils'
+import { plainText } from './composition/utils'
 import { BlockComponentProps } from './Blocks'
 
 interface ImageProps extends BlockComponentProps {
@@ -17,14 +18,6 @@ export const Image: JSXSlack.FC<ImageProps> = props => (
     alt_text={props.alt}
     block_id={props.id || props.blockId}
     image_url={props.src}
-    title={
-      props.title
-        ? {
-            type: 'plain_text',
-            text: props.title,
-            emoji: true, // TODO: Controlable emoji
-          }
-        : undefined
-    }
+    title={props.title ? plainText(props.title) : undefined}
   />
 )
diff --git a/src/block-kit/Modal.tsx b/src/block-kit/Modal.tsx
new file mode 100644
index 0000000..9620f81
--- /dev/null
+++ b/src/block-kit/Modal.tsx
@@ -0,0 +1,43 @@
+/** @jsx JSXSlack.h */
+import { View } from '@slack/types'
+import { JSXSlack } from '../jsx'
+import { ObjectOutput } from '../utils'
+import { Blocks, BlockComponentProps } from './Blocks'
+import { plainText } from './composition/utils'
+
+// TODO: Use original View type when supported fields for API on @slack/types
+type ViewForAPI = View & {
+  callback_id?: string
+  external_id?: string
+}
+
+export interface ModalProps {
+  callbackId?: string
+  children: JSXSlack.Children<BlockComponentProps>
+  clearOnClose?: boolean
+  close?: string
+  externalId?: string
+  notifyOnClose?: boolean
+  privateMetadata?: string
+  submit?: string
+  title: string
+}
+
+export const Modal: JSXSlack.FC<ModalProps> = props => (
+  <ObjectOutput<ViewForAPI>
+    blocks={JSXSlack(<Blocks children={props.children} />)}
+    callback_id={props.callbackId}
+    clear_on_close={
+      props.clearOnClose !== undefined ? props.clearOnClose : undefined
+    }
+    close={props.close ? plainText(props.close) : undefined}
+    external_id={props.externalId}
+    notify_on_close={
+      props.notifyOnClose !== undefined ? props.notifyOnClose : undefined
+    }
+    private_metadata={props.privateMetadata}
+    submit={props.submit ? plainText(props.submit) : undefined}
+    title={plainText(props.title)}
+    type="modal"
+  />
+)
diff --git a/src/block-kit/composition/Confirm.tsx b/src/block-kit/composition/Confirm.tsx
index 10d65ea..af30ee6 100644
--- a/src/block-kit/composition/Confirm.tsx
+++ b/src/block-kit/composition/Confirm.tsx
@@ -3,6 +3,7 @@ import { Confirm as SlackConfirm } from '@slack/types'
 import html from '../../html'
 import { JSXSlack } from '../../jsx'
 import { ObjectOutput } from '../../utils'
+import { plainText, mrkdwn } from './utils'
 
 export interface ConfirmProps {
   children: JSXSlack.Children<{}>
@@ -13,25 +14,9 @@ export interface ConfirmProps {
 
 export const Confirm: JSXSlack.FC<ConfirmProps> = props => (
   <ObjectOutput<SlackConfirm>
-    title={{
-      type: 'plain_text',
-      text: props.title,
-      emoji: true, // TODO: Controlable emoji
-    }}
-    text={{
-      type: 'mrkdwn',
-      text: html(props.children),
-      verbatim: true,
-    }}
-    confirm={{
-      type: 'plain_text',
-      text: props.confirm,
-      emoji: true, // TODO: Controlable emoji
-    }}
-    deny={{
-      type: 'plain_text',
-      text: props.deny,
-      emoji: true, // TODO: Controlable emoji
-    }}
+    title={plainText(props.title)}
+    text={mrkdwn(html(props.children))}
+    confirm={plainText(props.confirm)}
+    deny={plainText(props.deny)}
   />
 )
diff --git a/src/block-kit/composition/utils.ts b/src/block-kit/composition/utils.ts
new file mode 100644
index 0000000..27ea6b4
--- /dev/null
+++ b/src/block-kit/composition/utils.ts
@@ -0,0 +1,13 @@
+import { MrkdwnElement, PlainTextElement } from '@slack/types'
+
+export const plainText = (text: string): PlainTextElement => ({
+  type: 'plain_text',
+  text,
+  emoji: true, // TODO: Controlable emoji
+})
+
+export const mrkdwn = (mrkdwnText: string): MrkdwnElement => ({
+  type: 'mrkdwn',
+  text: mrkdwnText,
+  verbatim: true,
+})
diff --git a/src/block-kit/index.ts b/src/block-kit/index.ts
index 634f06d..bcc2d7b 100644
--- a/src/block-kit/index.ts
+++ b/src/block-kit/index.ts
@@ -1,5 +1,6 @@
 // Containers for Block Kit
 export { Blocks } from './Blocks'
+export { Modal } from './Modal'
 
 // Block Kit blocks
 export { Actions } from './Actions'
diff --git a/src/block-kit/interactive/Button.tsx b/src/block-kit/interactive/Button.tsx
index 0d46546..1ddf84e 100644
--- a/src/block-kit/interactive/Button.tsx
+++ b/src/block-kit/interactive/Button.tsx
@@ -3,6 +3,7 @@ import { Button as SlackButton } from '@slack/types'
 import { JSXSlack } from '../../jsx'
 import { ObjectOutput, PlainText } from '../../utils'
 import { ConfirmProps } from '../composition/Confirm'
+import { plainText } from '../composition/utils'
 
 export interface ButtonProps {
   actionId?: string
@@ -16,11 +17,7 @@ export interface ButtonProps {
 export const Button: JSXSlack.FC<ButtonProps> = props => (
   <ObjectOutput<SlackButton>
     type="button"
-    text={{
-      type: 'plain_text',
-      text: JSXSlack(<PlainText>{props.children}</PlainText>),
-      emoji: true, // TODO: Controlable emoji
-    }}
+    text={plainText(JSXSlack(<PlainText>{props.children}</PlainText>))}
     action_id={props.actionId}
     confirm={props.confirm ? JSXSlack(props.confirm) : undefined}
     style={props.style}
diff --git a/src/block-kit/interactive/DatePicker.tsx b/src/block-kit/interactive/DatePicker.tsx
index 1867ad1..8fd7b83 100644
--- a/src/block-kit/interactive/DatePicker.tsx
+++ b/src/block-kit/interactive/DatePicker.tsx
@@ -3,6 +3,7 @@ import { Datepicker } from '@slack/types'
 import { JSXSlack } from '../../jsx'
 import { ObjectOutput } from '../../utils'
 import { ConfirmProps } from '../composition/Confirm'
+import { plainText } from '../composition/utils'
 
 export interface DatePickerProps {
   actionId?: string
@@ -24,15 +25,7 @@ export const DatePicker: JSXSlack.FC<DatePickerProps> = props => (
     type="datepicker"
     action_id={props.actionId}
     confirm={props.confirm ? JSXSlack(props.confirm) : undefined}
-    placeholder={
-      props.placeholder
-        ? {
-            type: 'plain_text',
-            text: props.placeholder,
-            emoji: true, // TODO: Controlable emoji
-          }
-        : undefined
-    }
+    placeholder={props.placeholder ? plainText(props.placeholder) : undefined}
     initial_date={
       props.initialDate instanceof Date
         ? formatYMD(props.initialDate)
diff --git a/src/block-kit/interactive/Overflow.tsx b/src/block-kit/interactive/Overflow.tsx
index 6f00628..ba40c40 100644
--- a/src/block-kit/interactive/Overflow.tsx
+++ b/src/block-kit/interactive/Overflow.tsx
@@ -1,6 +1,7 @@
 /** @jsx JSXSlack.h */
 import { Option, Overflow as SlackOverflow } from '@slack/types'
 import { ConfirmProps } from '../composition/Confirm'
+import { plainText } from '../composition/utils'
 import { JSXSlack } from '../../jsx'
 import { ObjectOutput, PlainText } from '../../utils'
 
@@ -43,11 +44,7 @@ export const Overflow: JSXSlack.FC<OverflowProps> = props => {
       confirm={props.confirm ? JSXSlack(props.confirm) : undefined}
       options={opts.map(
         (o): Option => ({
-          text: {
-            type: 'plain_text',
-            text: o.props.text,
-            emoji: true, // TODO: Controlable emoji
-          },
+          text: plainText(o.props.text),
           ...(o.props.url ? { url: o.props.url } : {}),
           ...(o.props.value ? { value: o.props.value } : {}),
         })
diff --git a/src/block-kit/interactive/Select.tsx b/src/block-kit/interactive/Select.tsx
index 1c2d386..a9d8ac5 100644
--- a/src/block-kit/interactive/Select.tsx
+++ b/src/block-kit/interactive/Select.tsx
@@ -16,6 +16,7 @@ import {
 } from '@slack/types'
 import flattenDeep from 'lodash.flattendeep'
 import { ConfirmProps } from '../composition/Confirm'
+import { plainText } from '../composition/utils'
 import { JSXSlack } from '../../jsx'
 import {
   ObjectOutput,
@@ -159,18 +160,12 @@ const baseProps = (
   action_id: props.actionId,
   confirm: props.confirm ? JSXSlack(props.confirm) : undefined,
   max_selected_items: props.maxSelectedItems,
-  placeholder: props.placeholder
-    ? {
-        type: 'plain_text',
-        text: props.placeholder,
-        emoji: true, // TODO: Controlable emoji
-      }
-    : undefined,
+  placeholder: props.placeholder ? plainText(props.placeholder) : undefined,
 })
 
 const createOption = ({ value, text }: OptionInternal): SlackOption => ({
   value,
-  text: { text, type: 'plain_text', emoji: true }, // TODO: Controlable emoji
+  text: plainText(text),
 })
 
 const filter = <T extends {}>(children: JSXSlack.Children<T>) =>
@@ -219,11 +214,7 @@ export const SelectFragment: JSXSlack.FC<SelectFragmentProps> = props => {
         <ObjectOutput<SelectFragmentObject<'option_groups'>>
           option_groups={
             (opts as JSXSlack.Node<OptgroupInternal>[]).map(n => ({
-              label: {
-                type: 'plain_text',
-                text: n.props.label,
-                emoji: true, // TODO: Controlable emoji
-              },
+              label: plainText(n.props.label),
               options: filter(n.props.children).map(o => createOption(o.props)),
             })) as any
           }
diff --git a/src/dialog/Dialog.tsx b/src/dialog/Dialog.tsx
index 4e11ee6..67cd5cd 100644
--- a/src/dialog/Dialog.tsx
+++ b/src/dialog/Dialog.tsx
@@ -47,6 +47,7 @@ export const validateElement = (props: DialogElementProps) => {
   return { hint }
 }
 
+/** @deprecated A classic dialog support was deprecated in favor of Slack Modals. Please migrate into <Modal> container provided by main entrypoint. */
 export const Dialog: JSXSlack.FC<DialogProps> = props => {
   let { submitLabel } = props
   let stateJSON


Test patch:
diff --git a/test/index.tsx b/test/index.tsx
index c4cb4ed..c003651 100644
--- a/test/index.tsx
+++ b/test/index.tsx
@@ -7,6 +7,7 @@ import {
   SectionBlock,
   StaticSelect,
   Option as SlackOption,
+  View,
 } from '@slack/types'
 import JSXSlack, {
   Actions,
@@ -24,6 +25,7 @@ import JSXSlack, {
   File,
   Fragment,
   Image,
+  Modal,
   Optgroup,
   Option,
   Overflow,
@@ -48,6 +50,57 @@ describe('jsx-slack', () => {
           )
         ).toThrow())
     })
+
+    describe('<Modal>', () => {
+      it('generates view payload JSON', () => {
+        const simpleView: View = {
+          type: 'modal',
+          title: { type: 'plain_text', text: 'test', emoji: true },
+          blocks: [{ type: 'section', text: expect.any(Object) }],
+        }
+
+        expect(
+          JSXSlack(
+            <Modal title="test">
+              <Section>Hello!</Section>
+            </Modal>
+          )
+        ).toStrictEqual(simpleView)
+
+        // Optional attributes
+        const viewWithOptions: View & Record<string, any> = {
+          type: 'modal',
+          title: expect.any(Object),
+          blocks: expect.any(Array),
+          submit: { type: 'plain_text', text: 'Submit', emoji: true },
+          close: { type: 'plain_text', text: 'Close', emoji: true },
+          private_metadata: 'private_metadata',
+          clear_on_close: true,
+          notify_on_close: false,
+
+          // Fields for API
+          callback_id: 'callback_id',
+          external_id: 'external_id',
+        }
+
+        expect(
+          JSXSlack(
+            <Modal
+              callbackId="callback_id"
+              clearOnClose
+              close="Close"
+              externalId="external_id"
+              notifyOnClose={false}
+              privateMetadata="private_metadata"
+              submit="Submit"
+              title="test"
+            >
+              <Section>Hello!</Section>
+            </Modal>
+          )
+        ).toStrictEqual(viewWithOptions)
+      })
+    })
   })
 
   describe('Block Kit components', () => {