Repository: speee/jsx-slack (ts)

Issue:
Interactive components for radio button
Today Slack introduced new Block Kit blocks, includes radio buttons. Now developer seems to be able using radio button at ~~modal~~ app home.
https://api.slack.com/reference/block-kit/block-elements#radio

`radio_buttons` is available in following:

- ~~`<Modal>`~~
  - `<Actions>`
  - `<Section>`'s accessory
  - ~~`<Input>`'s element~~

I've tried to use `radio_buttons` element in modal but failed to open modal. Probably the reference of above URL is wrong.

By referring to https://api.slack.com/reference/block-kit/interactive-components, radio buttons have only supported in App Home (#75).

## ToDo

- [x] Consider interface
  - Based on existing `<Select>` component?
    - Internal spec is similar to `static_select` block elements.
  - or HTML-compatible `<input type="radio">` elements?
    - It may be better to implement later as aliases for the created interactive components.
- [x] Implement radio button interactive component(s)
  - [x] Interactive component
  - ~~[ ] Input component~~
- [x] Update documents

Pull request description:
Add <RadioButtonGroup> and <RadioButton> components
Added `<RadioButtonGroup>` and `<RadioButton>` interactive components. Resolves #74.

A following is an example of these components at onboarding process with home tab.

```jsx
<Home>
  <Section>
    :sparkles: <b>Wizard for getting started</b> (2/4)
  </Section>
  <Divider />
  <Section>
    Select the tier of our service:
    <RadioButtonGroup value="free">
      <RadioButton value="free" description="$0!">
        Free
      </RadioButton>
      <RadioButton value="standard" description="$5/month (with free 30 days trial!)">
        Standard
      </RadioButton>
      <RadioButton value="premium" description="$30/month">
        Premium
      </RadioButton>
      <RadioButton value="business" description="Please contact to support.">
        Business
      </RadioButton>
    </RadioButtonGroup>
  </Section>
  <Actions>
    <Button actionId="back">Back</Button>
    <Button actionId="next" style="primary">Next</Button>
  </Actions>
</Home>
```

[![](https://user-images.githubusercontent.com/3993388/67454475-2be3f400-f665-11e9-867d-55e14de15124.png)](https://api.slack.com/tools/block-kit-builder?mode=appHome&view=%7B%22type%22%3A%22home%22%2C%22blocks%22%3A%5B%7B%22type%22%3A%22section%22%2C%22text%22%3A%7B%22text%22%3A%22%3Asparkles%3A%20*Wizard%20for%20getting%20started*%20(2%2F4)%22%2C%22type%22%3A%22mrkdwn%22%2C%22verbatim%22%3Atrue%7D%7D%2C%7B%22type%22%3A%22divider%22%7D%2C%7B%22type%22%3A%22section%22%2C%22text%22%3A%7B%22text%22%3A%22Select%20the%20tier%20of%20our%20service%3A%22%2C%22type%22%3A%22mrkdwn%22%2C%22verbatim%22%3Atrue%7D%2C%22accessory%22%3A%7B%22type%22%3A%22radio_buttons%22%2C%22options%22%3A%5B%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Free%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22free%22%2C%22description%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%240!%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Standard%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22standard%22%2C%22description%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%245%2Fmonth%20(with%20free%2030%20days%20trial!)%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Premium%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22premium%22%2C%22description%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%2430%2Fmonth%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Business%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22business%22%2C%22description%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Please%20contact%20to%20support.%22%2C%22emoji%22%3Atrue%7D%7D%5D%2C%22initial_option%22%3A%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Free%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22free%22%2C%22description%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%240!%22%2C%22emoji%22%3Atrue%7D%7D%7D%7D%2C%7B%22type%22%3A%22actions%22%2C%22elements%22%3A%5B%7B%22type%22%3A%22button%22%2C%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Back%22%2C%22emoji%22%3Atrue%7D%2C%22action_id%22%3A%22back%22%7D%2C%7B%22type%22%3A%22button%22%2C%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Next%22%2C%22emoji%22%3Atrue%7D%2C%22action_id%22%3A%22next%22%2C%22style%22%3A%22primary%22%7D%5D%7D%5D%7D)

### Restriction

I've confirmed radio buttons can use only in home tab (`<Home>`), and can place only to `<Actions>` or `<Section>` accessory. [Slack explains it also can use in modal (`<Modal>`) and place to input block (`<Input>` and input component for modal)](https://api.slack.com/reference/block-kit/block-elements#radio), but neither is not true.

So we refactored `<Blocks>` to apply more strict validation by the parent container not only for children blocks but also the content of blocks. jsx-slack throws an error if you would use invalid interactive component in blocks.

### Interface

We have considered about using `checked` attribute similar to `<input type="radio">`, but we didn't adopt because it's hard to control checked radio button by using selected value. `checked` will require boring condition like `checked={props.value === 'xxx'}` for each radio buttons.

We are still planning HTML-compatible alias `<Input type="radio">` that is supported `checked` attribute. Perhaps it may become the second interface for using radio buttons.

### ToDo

- [x] Fill tests
- [x] Update references


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50dabb3..472fc79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### Added
 
 - Add (an experimental) `<Home>` container component for home tab ([#75](https://github.com/speee/jsx-slack/issues/75), [#78](https://github.com/speee/jsx-slack/pull/78))
+- [`<RadioButtonGroup>`](https://github.com/speee/jsx-slack/blob/master/docs/block-elements.md#radio-button-group) and [`<RadioButton>`](https://github.com/speee/jsx-slack/blob/master/docs/block-elements.md#radio-button) interactive component for home tab ([#74](https://github.com/speee/jsx-slack/issues/74), [#80](https://github.com/speee/jsx-slack/pull/80))
 - "Copy to clipboard" button on REPL demo ([#77](https://github.com/speee/jsx-slack/pull/77))
 
 ### Changed
diff --git a/README.md b/README.md
index 0a5478a..2d5411b 100644
--- a/README.md
+++ b/README.md
@@ -193,9 +193,14 @@ By using jsx-slack, you can build a template with piling up Block Kit blocks by
   <Divider />
   <Section>What's next?</Section>
   <Actions>
-    <Button actionId="tickets">See assigned tickets to me :ticket:</Button>
-    <Button actionId="reminder">Remind a task later :memo:</Button>
-    <Button actionId="pomodoro">Start pomodoro timer :tomato:</Button>
+    <RadioButtonGroup value="tickets">
+      <RadioButton value="tickets">See assigned tickets :ticket:</RadioButton>
+      <RadioButton value="reminder">Remind a task later :memo:</RadioButton>
+      <RadioButton value="pomodoro">Start pomodoro timer :tomato:</RadioButton>
+    </RadioButtonGroup>
+    <Button actionId="start" style="primary">
+      Start working
+    </Button>
   </Actions>
 </Home>
 ```
diff --git a/demo/example.js b/demo/example.js
index 630d147..0e9403e 100644
--- a/demo/example.js
+++ b/demo/example.js
@@ -53,9 +53,14 @@ export const home = `
   <Divider />
   <Section>What's next?</Section>
   <Actions>
-    <Button actionId="tickets">See assigned tickets to me :ticket:</Button>
-    <Button actionId="reminder">Remind a task later :memo:</Button>
-    <Button actionId="pomodoro">Start pomodoro timer :tomato:</Button>
+    <RadioButtonGroup value="tickets">
+      <RadioButton value="tickets">See assigned tickets :ticket:</RadioButton>
+      <RadioButton value="reminder">Remind a task later :memo:</RadioButton>
+      <RadioButton value="pomodoro">Start pomodoro timer :tomato:</RadioButton>
+    </RadioButtonGroup>
+    <Button actionId="start" style="primary">
+      Start working
+    </Button>
   </Actions>
 </Home>
 `.trim()
diff --git a/demo/index.js b/demo/index.js
index 33f4acb..aa39ba2 100644
--- a/demo/index.js
+++ b/demo/index.js
@@ -109,14 +109,16 @@ const setPreview = query => {
 const convert = () => {
   try {
     const output = jsxslack([jsxEditor.getValue()])
+    const encoded = encodeURIComponent(JSON.stringify(output))
+
     json.value = JSON.stringify(output, null, '  ')
 
     if (Array.isArray(output)) {
-      setPreview({ blocks: JSON.stringify(output), mode: 'message' })
+      setPreview({ blocks: encoded, mode: 'message' })
     } else if (output.type === 'modal') {
-      setPreview({ view: JSON.stringify(output), mode: 'modal' })
+      setPreview({ view: encoded, mode: 'modal' })
     } else if (output.type === 'home') {
-      setPreview({ view: JSON.stringify(output), mode: 'appHome' })
+      setPreview({ view: encoded, mode: 'appHome' })
     } else {
       setPreview(false)
     }
diff --git a/demo/schema.js b/demo/schema.js
index 46195c2..3cb9a81 100644
--- a/demo/schema.js
+++ b/demo/schema.js
@@ -8,6 +8,7 @@ const blockInteractiveComponents = [
   'ChannelsSelect',
   'Overflow',
   'DatePicker',
+  'RadioButtonGroup',
 
   // HTML compatible
   'button',
@@ -298,6 +299,11 @@ const schema = {
     },
     children: [],
   },
+  RadioButtonGroup: {
+    attrs: { value: null, ...blockInteractiveCommonAttrs },
+    children: ['RadioButton'],
+  },
+  RadioButton: { attrs: { value: null, description: null }, children: [] },
 
   // Composition objects
   Confirm: {
diff --git a/docs/block-elements.md b/docs/block-elements.md
index d62f9c2..d67e3ea 100644
--- a/docs/block-elements.md
+++ b/docs/block-elements.md
@@ -379,6 +379,49 @@ An easy way to let the user selecting any date is using `<DatePicker>` component
 - `title`/ `hint` (optional): Specify a helpful text appears under the element.
 - `required` (optional): A boolean prop to specify whether any value must be filled when user confirms modal.
 
+### <a name="radio-button-group" id="radio-button-group"></a> [`<RadioButtonGroup>`: Radio button group](https://api.slack.com/reference/block-kit/block-elements#radio) (Only for home tab)
+
+A container for grouping radio buttons. It provides easy control of the selected option through similar interface to [`<Select>`](#select).
+
+_This component is only for [`<Home>` container](block-containers.md#home)._
+
+```jsx
+<Home>
+  <Section>
+    Select the tier of our service:
+    <RadioButtonGroup actionId="tier" value="free">
+      <RadioButton value="free" description="$0!">
+        Free
+      </RadioButton>
+      <RadioButton value="standard" description="$5/month + 30 days trial!">
+        Standard
+      </RadioButton>
+      <RadioButton value="premium" description="$30/month">
+        Premium
+      </RadioButton>
+      <RadioButton value="business" description="Please contact to support.">
+        Business
+      </RadioButton>
+    </RadioButtonGroup>
+  </Section>
+</Home>
+```
+
+[<img src="https://raw.githubusercontent.com/speee/jsx-slack/master/docs/preview-btn.svg?sanitize=true" width="240" />](https://api.slack.com/tools/block-kit-builder?mode=appHome&view=%7B%22type%22%3A%22home%22%2C%22blocks%22%3A%5B%7B%22type%22%3A%22section%22%2C%22text%22%3A%7B%22text%22%3A%22Select%20the%20tier%20of%20our%20service%3A%22%2C%22type%22%3A%22mrkdwn%22%2C%22verbatim%22%3Atrue%7D%2C%22accessory%22%3A%7B%22type%22%3A%22radio_buttons%22%2C%22action_id%22%3A%22tier%22%2C%22options%22%3A%5B%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Free%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22free%22%2C%22description%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%240!%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Standard%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22standard%22%2C%22description%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%245%2Fmonth%20%2B%2030%20days%20trial!%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Premium%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22premium%22%2C%22description%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%2430%2Fmonth%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Business%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22business%22%2C%22description%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Please%20contact%20to%20support.%22%2C%22emoji%22%3Atrue%7D%7D%5D%2C%22initial_option%22%3A%7B%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Free%22%2C%22emoji%22%3Atrue%7D%2C%22value%22%3A%22free%22%2C%22description%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22%240!%22%2C%22emoji%22%3Atrue%7D%7D%7D%7D%5D%7D)
+
+#### Props
+
+- `name` / `actionId` (optional): An identifier for the action.
+- `value` (optional): A value for initial selected option. It must choose value from defined `<RadioButton>` elements in children.
+- `confirm` (optional): [`<Confirm>` element](#confirm) to show confirmation dialog.
+
+### <a name="radio-button" id="radio-button"></a> `<RadioButton>`: Radio button
+
+#### Props
+
+- `value` (**required**): A string value to send to Slack App when choose option.
+- `description` (optional): A description text for current radio button. It can see with faded color in just below main label.
+
 ## [Composition objects](https://api.slack.com/reference/messaging/composition-objects)
 
 ### <a name="confirm" id="confirm"></a> [`<Confirm>`: Confirmation dialog](https://api.slack.com/reference/messaging/composition-objects#confirm)
@@ -425,9 +468,9 @@ All of input components **must be placed as the children of `<Modal>`, and defin
 
 The list of input components is following:
 
-- [`<Input>`](#input) (`<input>`)
-- [`<Textarea>`](#textarea) (`<textarea>`)
-- [`<Select>`](#select) (`<select>`)
+- [`<Input>`](#input) / `<input>`
+- [`<Textarea>`](#textarea) / `<textarea>`
+- [`<Select>`](#select) / `<select>`
 - [`<ExternalSelect>`](#external-select)
 - [`<UsersSelect>`](#users-select)
 - [`<ConversationsSelect>`](#conversations-select)
diff --git a/docs/jsx-components-for-block-kit.md b/docs/jsx-components-for-block-kit.md
index 9735dec..4d9a55f 100644
--- a/docs/jsx-components-for-block-kit.md
+++ b/docs/jsx-components-for-block-kit.md
@@ -16,7 +16,7 @@
 - [**`<Image>`**: Image Block](layout-blocks.md#image)
 - [**`<Actions>`**: Actions Block](layout-blocks.md#actions)
 - [**`<Context>`**: Context Block](layout-blocks.md#context)
-- [**`<File>`**: File Block](layout-blocks.md#file)
+- [**`<File>`**: File Block](layout-blocks.md#file) (Only for messaging)
 - [**`<Input>`**: Input Block](layout-blocks.md#input) (Only for modal)
 
 ## **[Block elements](block-elements.md)**
@@ -35,6 +35,8 @@
 - [**`<Overflow>`**: Overflow menu](block-elements.md#overflow)
   - [**`<OverflowItem>`**: Menu item in overflow menu](block-elements.md#overflow-item)
 - [**`<DatePicker>`**: Select date from calendar](block-elements.md#date-picker)
+- [**`<RadioButtonGroup>`**: Radio button group](block-elements.md#radio-button-group) (Only for home tab)
+  - [**`<RadioButton>`**: Radio button](block-elements.md#radio-button)
 
 ### **[Composition objects](block-elements.md#composition-objects)**
 
diff --git a/docs/layout-blocks.md b/docs/layout-blocks.md
index 8f725fb..6b53b0f 100644
--- a/docs/layout-blocks.md
+++ b/docs/layout-blocks.md
@@ -4,7 +4,7 @@
 
 **[Layout blocks](https://api.slack.com/reference/block-kit/blocks)** are a series of basic blocks defined by Slack Block Kit.
 
-All of these blocks can use as the children of [all block containers](block-containers.md), unless there is specific requirements such as [`<Input>` block](#input).
+All of these blocks can use as the children of [all block containers](block-containers.md), unless there is specific requirements such as [`<File>`](#file) or [`<Input>`](#input).
 
 ## <a name="section" id="section"></a> [`<Section>`: Section Block](https://api.slack.com/reference/messaging/blocks#section)
 
@@ -43,15 +43,16 @@ A one of accessory component may include as the children of `<Section>`. The def
 
 #### Accessory components
 
-- [`<Image>`](#image) (`<img>`)
-- [`<Button>`](block-elements.md#button) (`<button>`)
-- [`<Select>`](block-elements.md#select) (`<select>`)
+- [`<Image>`](#image) / `<img>`
+- [`<Button>`](block-elements.md#button) / `<button>`
+- [`<Select>`](block-elements.md#select) / `<select>`
 - [`<ExternalSelect>`](block-elements.md#external-select)
 - [`<UsersSelect>`](block-elements.md#users-select)
 - [`<ConversationsSelect>`](block-elements.md#conversations-select)
 - [`<ChannelsSelect>`](block-elements.md#channels-select)
 - [`<Overflow>`](block-elements.md#overflow)
 - [`<DatePicker>`](block-elements.md#date-picker)
+- [`<RadioButtonGroup>`](block-elements.md#radio-button-group) (Only for [`<Home>` container](block-containers.md#home))
 
 ### <a name="field" id="field"></a> `<Field>`: Fields for section block
 
@@ -170,9 +171,9 @@ Text contents will merge in pertinent mrkdwn elements automatically, but they al
 
 - `id` / `blockId` (optional): A string of unique identifier of block.
 
-## <a name="file" id="file"></a> [`<File>`: File Block](https://api.slack.com/reference/messaging/blocks#file)
+## <a name="file" id="file"></a> [`<File>`: File Block](https://api.slack.com/reference/messaging/blocks#file) (Only for messaging)
 
-Display a remote file that was added to Slack workspace. [Learn about adding remote files in the document of Slack API.](https://api.slack.com/messaging/files/remote)
+Display a remote file that was added to Slack workspace. [Learn about adding remote files in the document of Slack API.](https://api.slack.com/messaging/files/remote) _This block is only for [`<Blocks>` container](block-containers.md#blocks)._
 
 ```jsx
 <Blocks>
@@ -188,7 +189,7 @@ Display a remote file that was added to Slack workspace. [Learn about adding rem
 
 ## <a name="input" id="input"></a> [`<Input>`: Input Block](https://api.slack.com/reference/messaging/blocks#input) (Only for modal)
 
-Display one of interactive components for input to collect information from users. _This block is only for [`<Modal>`](block-containers.md#modal)._
+Display one of interactive components for input to collect information from users. _This block is only for [`<Modal>` container](block-containers.md#modal)._
 
 If you want to use `<Input>` as layout block, you have to place one of [available interactive components](#available-interactive-components) as a child.
 
diff --git a/src/block-kit/Actions.tsx b/src/block-kit/Actions.tsx
index 65ee912..fc42e0a 100644
--- a/src/block-kit/Actions.tsx
+++ b/src/block-kit/Actions.tsx
@@ -14,7 +14,7 @@ interface ActionsProps extends BlockComponentProps {
   >
 }
 
-const actionTypes = [
+export const actionTypes = [
   'button',
   'static_select',
   'external_select',
@@ -23,7 +23,8 @@ const actionTypes = [
   'channels_select',
   'overflow',
   'datepicker',
-]
+  'radio_buttons',
+] as const
 
 export const Actions: JSXSlack.FC<ActionsProps> = props => {
   const children = JSXSlack.normalizeChildren(props.children).map(child => {
diff --git a/src/block-kit/Blocks.tsx b/src/block-kit/Blocks.tsx
index e8d439b..02637e6 100644
--- a/src/block-kit/Blocks.tsx
+++ b/src/block-kit/Blocks.tsx
@@ -1,9 +1,13 @@
 /** @jsx JSXSlack.h */
-import { JSXSlack, jsxOnParsed } from '../jsx'
-import { ArrayOutput, aliasTo, isNode, wrap } from '../utils'
+import { JSXSlack } from '../jsx'
+import { ArrayOutput, aliasTo, isNode } from '../utils'
+import { actionTypes } from './Actions'
 import { internalHiddenType, internalSubmitType } from './Input'
+import { sectionAccessoryTypes } from './Section'
 import { Divider, Image, Input, Section, Select, Textarea } from './index'
 
+type KnownMap = Map<InternalBlockType | undefined, (string | symbol)[]>
+
 export interface BlocksProps {
   children: JSXSlack.Children<BlockComponentProps>
 }
@@ -20,15 +24,19 @@ export enum InternalBlockType {
 
 export const blockTypeSymbol = Symbol('jsx-slack-block-type')
 
-const knownBlocks: Map<
-  InternalBlockType | undefined,
-  (string | symbol)[]
-> = new Map()
+const knownBlocks: KnownMap = new Map()
+const knownActions: KnownMap = new Map()
+const knownSectionAccessories: KnownMap = new Map()
 
 const basicBlocks = ['actions', 'context', 'divider', 'image', 'section']
 
 // Blocks
 knownBlocks.set(undefined, [...basicBlocks, 'file'])
+knownActions.set(undefined, actionTypes.filter(t => t !== 'radio_buttons'))
+knownSectionAccessories.set(
+  undefined,
+  sectionAccessoryTypes.filter(t => t !== 'radio_buttons')
+)
 
 // Modal
 knownBlocks.set(InternalBlockType.Modal, [
@@ -37,15 +45,30 @@ knownBlocks.set(InternalBlockType.Modal, [
   internalHiddenType,
   internalSubmitType,
 ])
+knownActions.set(
+  InternalBlockType.Modal,
+  actionTypes.filter(t => t !== 'radio_buttons')
+)
+knownSectionAccessories.set(
+  InternalBlockType.Modal,
+  sectionAccessoryTypes.filter(t => t !== 'radio_buttons')
+)
 
 // Home
 knownBlocks.set(InternalBlockType.Home, basicBlocks)
+knownActions.set(InternalBlockType.Home, [...actionTypes])
+knownSectionAccessories.set(InternalBlockType.Home, [...sectionAccessoryTypes])
 
 export const Blocks: JSXSlack.FC<BlocksProps> = props => {
   const internalType: InternalBlockType | undefined = props[blockTypeSymbol]
 
-  const normalized = wrap(props.children).map(child => {
-    if (child && isNode(child) && typeof child.type === 'string') {
+  const availableBlocks = knownBlocks.get(internalType) || []
+  const availableActions = knownActions.get(internalType) || []
+  const availableSectionAccessories =
+    knownSectionAccessories.get(internalType) || []
+
+  const normalized = JSXSlack.normalizeChildren(props.children).map(child => {
+    if (isNode(child) && typeof child.type === 'string') {
       // Aliasing intrinsic elements to Block component
       switch (child.type) {
         case 'hr':
@@ -69,19 +92,40 @@ export const Blocks: JSXSlack.FC<BlocksProps> = props => {
     return child
   })
 
-  const node = <ArrayOutput>{normalized}</ArrayOutput>
+  for (const child of normalized) {
+    let valid = false
+
+    if (isNode(child) && child.type === JSXSlack.NodeType.object) {
+      if (availableBlocks.includes(child.props.type)) {
+        // Validate compatibillity of actions and section accessory with container
+        if (
+          child.props.type === 'actions' &&
+          child.props.elements.some(e => !availableActions.includes(e.type))
+        )
+          throw new Error(
+            '<Actions> block has an incompatible element with container.'
+          )
+
+        if (
+          child.props.type === 'section' &&
+          child.props.accessory &&
+          !availableSectionAccessories.includes(child.props.accessory.type)
+        )
+          throw new Error(
+            '<Section> block has an incompatible element with container.'
+          )
 
-  node.props[jsxOnParsed] = parsed => {
-    // Check the final output
-    const availableBlocks = knownBlocks.get(internalType) || []
+        valid = true
+      }
+    }
 
-    if (parsed.some(b => !availableBlocks.includes(b.type)))
+    if (!valid)
       throw new Error(
         'Block container allows only including valid layout block components.'
       )
   }
 
-  return node
+  return <ArrayOutput>{normalized}</ArrayOutput>
 }
 
 export const BlocksInternal: JSXSlack.FC<
diff --git a/src/block-kit/Input.tsx b/src/block-kit/Input.tsx
index df1d898..4a39f7d 100644
--- a/src/block-kit/Input.tsx
+++ b/src/block-kit/Input.tsx
@@ -1,6 +1,6 @@
 /** @jsx JSXSlack.h */
 import { InputBlock } from '@slack/types'
-import { JSXSlack, jsxOnParsed } from '../jsx'
+import { JSXSlack } from '../jsx'
 import { ObjectOutput, coerceToInteger } from '../utils'
 import { BlockComponentProps } from './Blocks'
 import { plainText } from './composition/utils'
@@ -165,24 +165,21 @@ export const Input: JSXSlack.FC<InputProps> = props => {
   }
 
   const hintText = props.hint || props.title
-  const node = (
+  const element = JSXSlack(props.children)
+
+  if (!(typeof element === 'object' && knownInputs.includes(element.type)))
+    throw new Error('A wrapped element in <Input> is invalid.')
+
+  return (
     <ObjectOutput<InputBlock>
       type="input"
       block_id={props.id || props.blockId}
       hint={hintText ? plainText(hintText) : undefined}
       label={plainText(props.label)}
       optional={!props.required}
-      element={JSXSlack(props.children)}
+      element={element}
     />
   )
-
-  node.props[jsxOnParsed] = parsed => {
-    // Check the final output
-    if (!(parsed.element && knownInputs.includes(parsed.element.type)))
-      throw new Error('A wrapped element in <Input> is invalid.')
-  }
-
-  return node
 }
 
 export const Textarea: JSXSlack.FC<TextareaProps> = props =>
diff --git a/src/block-kit/Section.tsx b/src/block-kit/Section.tsx
index f66248b..009a122 100644
--- a/src/block-kit/Section.tsx
+++ b/src/block-kit/Section.tsx
@@ -9,6 +9,24 @@ import { Image } from './Image'
 import { Button } from './elements/Button'
 import { Select } from './elements/Select'
 
+export const sectionAccessoryTypes = [
+  'image',
+  'button',
+  'static_select',
+  'external_select',
+  'users_select',
+  'conversations_select',
+  'channels_select',
+  'multi_static_select',
+  'multi_external_select',
+  'multi_users_select',
+  'multi_conversations_select',
+  'multi_channels_select',
+  'overflow',
+  'datepicker',
+  'radio_buttons',
+] as const
+
 export const Section: JSXSlack.FC<
   BlockComponentProps & { children: JSXSlack.Children<{}> }
 > = ({ blockId, children, id }) => {
@@ -23,31 +41,13 @@ export const Section: JSXSlack.FC<
     if (typeof child === 'object') {
       // Accessory and fields
       if (child.type === JSXSlack.NodeType.object) {
-        switch (child.props.type) {
-          case 'image':
-          case 'button':
-          case 'static_select':
-          case 'external_select':
-          case 'users_select':
-          case 'conversations_select':
-          case 'channels_select':
-          case 'multi_static_select':
-          case 'multi_external_select':
-          case 'multi_users_select':
-          case 'multi_conversations_select':
-          case 'multi_channels_select':
-          case 'overflow':
-          case 'datepicker':
-            accessory = JSXSlack(child)
-            break
-          case 'mrkdwn':
-            if (!fields) fields = []
-            fields.push(child.props)
-            break
-          default:
-            throw new Error(
-              '<Section> has unexpected component as an accessory.'
-            )
+        if (sectionAccessoryTypes.includes(child.props.type)) {
+          accessory = JSXSlack(child)
+        } else if (child.props.type === 'mrkdwn') {
+          if (!fields) fields = []
+          fields.push(child.props)
+        } else {
+          throw new Error('<Section> has unexpected component as an accessory.')
         }
         eaten = true
       } else if (child.type === 'img') {
diff --git a/src/block-kit/elements/RadioButton.tsx b/src/block-kit/elements/RadioButton.tsx
new file mode 100644
index 0000000..7a34ad0
--- /dev/null
+++ b/src/block-kit/elements/RadioButton.tsx
@@ -0,0 +1,79 @@
+/** @jsx JSXSlack.h */
+import { Option } from '@slack/types'
+import { ConfirmProps } from '../composition/Confirm'
+import { plainText } from '../composition/utils'
+import { JSXSlack } from '../../jsx'
+import { ObjectOutput, PlainText, isNode } from '../../utils'
+
+const radioButtonInternal = Symbol('radioButtonInternal')
+
+export interface RadioButtonGroupProps {
+  actionId?: string
+  children?: JSXSlack.Children<any>
+  confirm?: JSXSlack.Node<ConfirmProps>
+  name?: string
+  value?: string
+}
+
+export interface RadioButtonProps {
+  children: JSXSlack.Children<{}>
+  description?: string
+  value: string
+}
+
+interface RadioButtonInternal extends RadioButtonProps {
+  label: string
+  type: typeof radioButtonInternal
+}
+
+const filterRadioButtons = children =>
+  JSXSlack.normalizeChildren(children).filter(
+    c =>
+      isNode(c) &&
+      c.type === JSXSlack.NodeType.object &&
+      c.props.type === radioButtonInternal
+  ) as JSXSlack.Node<RadioButtonInternal>[]
+
+const toOptionObject = (props: RadioButtonInternal): Option => {
+  const option: Option = {
+    text: plainText(props.label),
+    value: props.value,
+  }
+
+  if (props.description) option.description = plainText(props.description)
+
+  return option
+}
+
+export const RadioButtonGroup: JSXSlack.FC<RadioButtonGroupProps> = props => {
+  const options = filterRadioButtons(props.children).map(radio =>
+    toOptionObject(radio.props)
+  )
+
+  if (options.length === 0)
+    throw new Error(
+      '<RadioButtonGroup> must include least of one <RadioButton>.'
+    )
+
+  const initialOption = props.value
+    ? options.find(o => o.value === props.value)
+    : undefined
+
+  return (
+    <ObjectOutput
+      type="radio_buttons"
+      action_id={props.actionId || props.name}
+      options={options}
+      initial_option={initialOption}
+      confirm={props.confirm ? JSXSlack(props.confirm) : undefined}
+    />
+  )
+}
+
+export const RadioButton: JSXSlack.FC<RadioButtonProps> = props => (
+  <ObjectOutput<RadioButtonInternal>
+    {...props}
+    type={radioButtonInternal}
+    label={JSXSlack(<PlainText>{props.children}</PlainText>)}
+  />
+)
diff --git a/src/block-kit/index.ts b/src/block-kit/index.ts
index 4c4cd2e..1176425 100644
--- a/src/block-kit/index.ts
+++ b/src/block-kit/index.ts
@@ -26,6 +26,7 @@ export {
 } from './elements/Select'
 export { Overflow, OverflowItem } from './elements/Overflow'
 export { DatePicker } from './elements/DatePicker'
+export { RadioButtonGroup, RadioButton } from './elements/RadioButton'
 
 // PlainTextInput won't provide because Input block has an usage as component.
 // export { PlainTextInput } from './elements/PlainTextInput'
diff --git a/src/jsx.ts b/src/jsx.ts
index 88ba0d7..062cb12 100644
--- a/src/jsx.ts
+++ b/src/jsx.ts
@@ -27,17 +27,12 @@ export interface ParseContext {
   mode: ParseMode
 }
 
-export const jsxOnParsed = Symbol('jsx-slack-jsxOnParsed')
-
 export function JSXSlack(node: JSXSlack.Node) {
   // eslint-disable-next-line @typescript-eslint/no-use-before-define
   return parseJSX(node, { builts: [], elements: [], mode: ParseMode.normal })
 }
 
-function parseJSX(
-  node: JSXSlack.Node & { [jsxOnParsed]?: OnParsed },
-  context: ParseContext
-) {
+function parseJSX(node: JSXSlack.Node, context: ParseContext) {
   const children = JSXSlack.normalizeChildren(
     node.props.children || node.children || []
   )
@@ -57,41 +52,34 @@ function parseJSX(
       [] as any[]
     )
 
-  const ret = (() => {
-    switch (node.type) {
-      case JSXSlack.NodeType.object:
-        return Object.keys(node.props).reduce(
-          (obj, k) =>
-            node.props[k] !== undefined ? { ...obj, [k]: node.props[k] } : obj,
-          {}
-        )
-      case JSXSlack.NodeType.array:
-        return toArray()
-      case JSXSlack.NodeType.html:
-        return turndown(toArray({ ...context, mode: ParseMode.HTML }).join(''))
-      case JSXSlack.NodeType.escapeInHtml:
-        return escapeChars(toArray().join(''))
-      case JSXSlack.NodeType.string:
-        return toArray({ ...context, mode: ParseMode.plainText }).join('')
-      default:
-        if (typeof node.type === 'string') {
-          context.elements.push(node.type)
-
-          try {
-            if (context.mode === ParseMode.plainText) return toArray()
-            return parse(node.type, node.props, toArray(), context)
-          } finally {
-            context.elements.pop()
-          }
+  switch (node.type) {
+    case JSXSlack.NodeType.object:
+      return Object.keys(node.props).reduce(
+        (obj, k) =>
+          node.props[k] !== undefined ? { ...obj, [k]: node.props[k] } : obj,
+        {}
+      )
+    case JSXSlack.NodeType.array:
+      return toArray()
+    case JSXSlack.NodeType.html:
+      return turndown(toArray({ ...context, mode: ParseMode.HTML }).join(''))
+    case JSXSlack.NodeType.escapeInHtml:
+      return escapeChars(toArray().join(''))
+    case JSXSlack.NodeType.string:
+      return toArray({ ...context, mode: ParseMode.plainText }).join('')
+    default:
+      if (typeof node.type === 'string') {
+        context.elements.push(node.type)
+
+        try {
+          if (context.mode === ParseMode.plainText) return toArray()
+          return parse(node.type, node.props, toArray(), context)
+        } finally {
+          context.elements.pop()
         }
-        throw new Error(`Unknown node type: ${node.type}`)
-    }
-  })()
-
-  if (node.props[jsxOnParsed] !== undefined)
-    node.props[jsxOnParsed](ret, context)
-
-  return ret
+      }
+      throw new Error(`Unknown node type: ${node.type}`)
+  }
 }
 
 // eslint-disable-next-line no-redeclare


Test patch:
diff --git a/test/block-kit/block-elements/interactive-components.tsx b/test/block-kit/block-elements/interactive-components.tsx
index c5a1437..a15aebd 100644
--- a/test/block-kit/block-elements/interactive-components.tsx
+++ b/test/block-kit/block-elements/interactive-components.tsx
@@ -14,10 +14,14 @@ import JSXSlack, {
   ConversationsSelect,
   DatePicker,
   ExternalSelect,
+  Home,
+  Modal,
   Optgroup,
   Option,
   Overflow,
   OverflowItem,
+  RadioButton,
+  RadioButtonGroup,
   Section,
   Select,
   UsersSelect,
@@ -557,7 +561,7 @@ describe('Interactive components', () => {
         )
       ).toStrictEqual([action(baseOverflow)])
 
-      // confirm prop and HTML-compatible props
+      // "confirm" prop and HTML-compatible props
       expect(
         JSXSlack(
           <Blocks>
@@ -643,4 +647,169 @@ describe('Interactive components', () => {
       ).toStrictEqual([datePickerAction])
     })
   })
+
+  describe('<RadioButtonGroup>', () => {
+    it('outputs radio button group in actions block', () => {
+      const radioButtonAction = {
+        type: 'radio_buttons',
+        action_id: 'radio-buttons',
+        options: [
+          {
+            text: { type: 'plain_text', text: '1st', emoji: true },
+            description: {
+              type: 'plain_text',
+              text: 'The first option',
+              emoji: true,
+            },
+            value: 'first',
+          },
+          {
+            text: { type: 'plain_text', text: '2nd', emoji: true },
+            description: {
+              type: 'plain_text',
+              text: 'The second option',
+              emoji: true,
+            },
+            value: 'second',
+          },
+          {
+            text: { type: 'plain_text', text: '3rd', emoji: true },
+            value: 'third',
+          },
+        ],
+        initial_option: {
+          text: { type: 'plain_text', text: '2nd', emoji: true },
+          description: {
+            type: 'plain_text',
+            text: 'The second option',
+            emoji: true,
+          },
+          value: 'second',
+        },
+      }
+
+      expect(
+        JSXSlack(
+          <Home>
+            <Actions blockId="actions">
+              <RadioButtonGroup actionId="radio-buttons" value="second">
+                <RadioButton value="first" description="The first option">
+                  1st
+                </RadioButton>
+                <RadioButton value="second" description="The second option">
+                  2nd
+                </RadioButton>
+                <RadioButton value="third">3rd</RadioButton>
+              </RadioButtonGroup>
+            </Actions>
+          </Home>
+        ).blocks
+      ).toStrictEqual([action(radioButtonAction)])
+
+      // "confirm" prop and HTML-compatible props
+      expect(
+        JSXSlack(
+          <Home>
+            <Actions id="actions">
+              <RadioButtonGroup
+                name="radio-buttons"
+                value="second"
+                confirm={
+                  <Confirm title="a" confirm="b" deny="c">
+                    foobar
+                  </Confirm>
+                }
+              >
+                <RadioButton value="first" description="The first option">
+                  1st
+                </RadioButton>
+                <RadioButton value="second" description="The second option">
+                  2nd
+                </RadioButton>
+                <RadioButton value="third">3rd</RadioButton>
+              </RadioButtonGroup>
+            </Actions>
+          </Home>
+        ).blocks
+      ).toStrictEqual([
+        action({
+          ...radioButtonAction,
+          confirm: {
+            title: { type: 'plain_text', text: 'a', emoji: true },
+            confirm: { type: 'plain_text', text: 'b', emoji: true },
+            deny: { type: 'plain_text', text: 'c', emoji: true },
+            text: { type: 'mrkdwn', text: 'foobar', verbatim: true },
+          },
+        } as any),
+      ])
+    })
+
+    it('outputs radio button group in section block', () => {
+      const [section]: SectionBlock[] = JSXSlack(
+        <Home>
+          <Section>
+            test
+            <RadioButtonGroup>
+              <RadioButton value="a">A</RadioButton>
+            </RadioButtonGroup>
+          </Section>
+        </Home>
+      ).blocks
+
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      expect(section.accessory!.type).toBe('radio_buttons')
+    })
+
+    it('throws error when <RadioButtonGroup> has not contained <RadioButton>', () => {
+      expect(() =>
+        JSXSlack(
+          <Home>
+            <Actions>
+              <RadioButtonGroup>{}</RadioButtonGroup>
+            </Actions>
+          </Home>
+        )
+      ).toThrow(/must include/i)
+
+      expect(() =>
+        JSXSlack(
+          <Home>
+            <Actions>
+              <RadioButtonGroup>
+                <Option value="wtf">I'm not radio button</Option>
+              </RadioButtonGroup>
+            </Actions>
+          </Home>
+        )
+      ).toThrow(/must include/i)
+    })
+
+    it('throws error when using <RadioButtonGroup> within invalid container', () => {
+      const invalidContainers: JSXSlack.FC<{ children: any }>[] = [
+        Blocks,
+        ({ children }) => <Modal title="test">{children}</Modal>,
+      ]
+
+      const invalidBlocks: JSXSlack.FC<{ children: any }>[] = [
+        Actions,
+        ({ children }) => <Section>test{children}</Section>,
+      ]
+
+      for (const Container of invalidContainers) {
+        for (const Block of invalidBlocks) {
+          expect(() =>
+            JSXSlack(
+              <Container>
+                <Block>
+                  <RadioButtonGroup>
+                    <RadioButton value="a">A</RadioButton>
+                  </RadioButtonGroup>
+                </Block>
+              </Container>
+            )
+          ).toThrow(/incompatible/i)
+        }
+      }
+    })
+  })
 })
diff --git a/test/index.tsx b/test/index.tsx
index 3f58f99..4d83c0a 100644
--- a/test/index.tsx
+++ b/test/index.tsx
@@ -1,24 +1,9 @@
 /** @jsx JSXSlack.h */
-import JSXSlack, { Option, SelectFragment } from '../src/index'
-import { jsxOnParsed } from '../src/jsx'
+import JSXSlack from '../src/index'
 
 beforeEach(() => JSXSlack.exactMode(false))
 
 describe('#JSXSlack', () => {
-  it('executes specified func after parsing if passed internal node has jsxOnParsed symbol prop', () => {
-    const onParsed = jest.fn()
-    const node = (
-      <SelectFragment>
-        <Option value="a">A</Option>
-      </SelectFragment>
-    )
-
-    node.props[jsxOnParsed] = onParsed
-    JSXSlack(node)
-
-    expect(onParsed).toBeCalledTimes(1)
-  })
-
   it('throws error by passed invalid node', () =>
     expect(() => JSXSlack({ props: {}, type: -1 } as any)).toThrow())
 })