Repository: speee/jsx-slack
Pull request #65 (2019-10-01 08:23:49)

Add intrinsic HTML elements of input components
This PR has implemented a part of #57.

We've added these intrinsic HTML elements:

- `<button>`
- `<input>`
- `<optgroup>`
- `<option>`
- `<select>`
- `<textarea>`

These will work as alias of built-in components in the right place.

While using TypeScript, props that has HTML-compatible alias are hidden from type definition of intrinsic elements.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2ef513..4dade32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
 - `<Input>` component for layout block and block element ([#61](https://github.com/speee/jsx-slack/pull/61))
 - `<Textarea>` component ([#62](https://github.com/speee/jsx-slack/pull/62))
 - Input-compatible props to select-like elements and `<DatePicker>` ([#63](https://github.com/speee/jsx-slack/pull/63))
+- Intrinsic HTML elements of input components ([#65](https://github.com/speee/jsx-slack/pull/65))
 
 ### Changed
 
diff --git a/docs/jsx-components-for-block-kit.md b/docs/jsx-components-for-block-kit.md
index 9719406..41efe35 100644
--- a/docs/jsx-components-for-block-kit.md
+++ b/docs/jsx-components-for-block-kit.md
@@ -269,13 +269,15 @@ If you want to use `<Input>` as layout block, you have to place the one of avail
 - [`<ChannelsSelect>`](#channelsselect-select-menu-with-channel-list)
 - [`<DatePicker>`](#datepicker-select-date-from-calendar)
 
+> :information_source: By passing props for input layout block, these available elements and [`<Input>`](#input-element) / [`<Textarea>`](#textarea-plain-text-input-element-with-multiline-only-for-modal) block element can place to modal directly and write JSX template with familiar HTML form style. `<Input>` layout block is provided for user that want templating with Slack API style rather than HTML style.
+
 ## Block elements
 
 Some blocks may include block elements. e.g. the interactive component to exchange info with Slack app.
 
 ### [`<Button>`: Button element](https://api.slack.com/reference/messaging/block-elements#button)
 
-A simple button to send action to registered Slack App, or open external URL.
+A simple button to send action to registered Slack App, or open external URL. `<button>` intrinsic HTML element also works as well.
 
 ```jsx
 <Blocks>
@@ -292,7 +294,7 @@ A simple button to send action to registered Slack App, or open external URL.
 
 #### Props
 
-- `actionId` (optional): An identifier for the action.
+- `name` / `actionId` (optional): An identifier for the action.
 - `value` (optional): A string value to send to Slack App when clicked button.
 - `url` (optional): URL to load when clicked button.
 - `style` (optional): Select the colored button decoration from `primary` and `danger`.
@@ -300,7 +302,7 @@ A simple button to send action to registered Slack App, or open external URL.
 
 ### [`<Select>`: Select menu with static options](https://api.slack.com/reference/messaging/block-elements#static-select)
 
-A menu element with static options passed by `<Option>` or `<Optgroup>`. It has a interface similar to `<select>` HTML element.
+A menu element with static options passed by `<Option>` or `<Optgroup>`. It has a interface similar to `<select>` HTML element. In fact, `<select>` intrinsic HTML element works as well.
 
 ```jsx
 <Blocks>
@@ -405,12 +407,16 @@ The above JSX means exactly same as following:
 
 #### `<Option>`: Menu item
 
+Define an item for `<Select>`. `<option>` intrinsic HTML element works as well.
+
 ##### Props
 
 - `value` (**required**): A string value to send to Slack App when choose item.
 
 #### `<Optgroup>`: Group of menu items
 
+Define a group for `<Select>`. `<optgroup>` intrinsic HTML element works as well.
+
 ```jsx
 <Blocks>
   <Actions>
@@ -589,7 +595,7 @@ An overflow menu displayed as `...` can access to some hidden menu items by many
 
 #### Props
 
-- `actionId` (optional): An identifier for the action.
+- `name` / `actionId` (optional): An identifier for the action.
 - `confirm` (optional): [`<Confirm>` element](#confirm-confirmation-dialog) to show confirmation dialog when clicked menu item.
 
 #### `<OverflowItem>`: Menu item in overflow menu
@@ -651,7 +657,9 @@ It can place as children of `<Modal>` directly.
 
 [<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?blocks=%5B%7B%22type%22%3A%22input%22%2C%22label%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Title%22%2C%22emoji%22%3Atrue%7D%2C%22optional%22%3Afalse%2C%22element%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22action_id%22%3A%22title%22%2C%22max_length%22%3A80%7D%7D%5D&mode=modal)
 
-> Internally this is syntactic sugar for [`<Input>` block](#input-block) with a plain-text input.
+`<input>` intrinsic HTML element also works as well.
+
+> :information_source: Internally this is syntactic sugar for [`<Input>` block](#input-block) with [a plain-text input](https://api.slack.com/reference/block-kit/block-elements#input).
 
 #### <a name="input-element-props" id="input-element-props">Props (Block element)</a>
 
@@ -683,9 +691,11 @@ It can place as children of `<Modal>` directly.
 
 [<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?blocks=%5B%7B%22type%22%3A%22input%22%2C%22label%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Tweet%22%2C%22emoji%22%3Atrue%7D%2C%22optional%22%3Afalse%2C%22element%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22action_id%22%3A%22tweet%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22What%E2%80%99s%20happening%3F%22%2C%22emoji%22%3Afalse%7D%2C%22multiline%22%3Atrue%2C%22max_length%22%3A280%7D%7D%5D&mode=modal)
 
+`<textarea>` intrinsic HTML element also works as well.
+
 #### Props
 
-It is exactly same as [`<Input>` block element](#input-element-props).
+It's exactly same as [`<Input>` block element](#input-element-props).
 
 ## Components for [composition objects](https://api.slack.com/reference/messaging/composition-objects)
 
diff --git a/src/block-kit/Actions.tsx b/src/block-kit/Actions.tsx
index 2260ec7..65ee912 100644
--- a/src/block-kit/Actions.tsx
+++ b/src/block-kit/Actions.tsx
@@ -1,10 +1,10 @@
 /** @jsx JSXSlack.h */
 import { ActionsBlock } from '@slack/types'
 import { JSXSlack } from '../jsx'
-import { ArrayOutput, ObjectOutput } from '../utils'
+import { ArrayOutput, ObjectOutput, aliasTo, isNode } from '../utils'
 import { BlockComponentProps } from './Blocks'
-import { ButtonProps } from './elements/Button'
-import { SingleSelectPropsBase } from './elements/Select'
+import { Button, ButtonProps } from './elements/Button'
+import { Select, SingleSelectPropsBase } from './elements/Select'
 import { OverflowProps } from './elements/Overflow'
 import { DatePickerBaseProps } from './elements/DatePicker'
 
@@ -26,7 +26,15 @@ const actionTypes = [
 ]
 
 export const Actions: JSXSlack.FC<ActionsProps> = props => {
-  const elements = JSXSlack(<ArrayOutput>{props.children}</ArrayOutput>)
+  const children = JSXSlack.normalizeChildren(props.children).map(child => {
+    if (isNode(child)) {
+      if (child.type === 'button') return aliasTo(Button, child)
+      if (child.type === 'select') return aliasTo(Select, child)
+    }
+    return child
+  })
+
+  const elements = JSXSlack(<ArrayOutput>{children}</ArrayOutput>)
 
   if (elements.length > 25)
     throw new Error(
diff --git a/src/block-kit/Blocks.tsx b/src/block-kit/Blocks.tsx
index dcb137c..8dca9fd 100644
--- a/src/block-kit/Blocks.tsx
+++ b/src/block-kit/Blocks.tsx
@@ -1,7 +1,7 @@
 /** @jsx JSXSlack.h */
 import { JSXSlack, jsxOnParsed } from '../jsx'
-import { ArrayOutput, isNode, wrap } from '../utils'
-import { Divider, Image, Section } from './index'
+import { ArrayOutput, aliasTo, isNode, wrap } from '../utils'
+import { Divider, Image, Input, Section, Select, Textarea } from './index'
 
 export interface BlocksProps {
   children: JSXSlack.Children<BlockComponentProps>
@@ -36,11 +36,17 @@ export const Blocks: JSXSlack.FC<BlocksProps> = props => {
       // Aliasing intrinsic elements to Block component
       switch (child.type) {
         case 'hr':
-          return <Divider {...child.props}>{...child.children}</Divider>
+          return aliasTo(Divider, child)
         case 'img':
-          return <Image {...child.props}>{...child.children}</Image>
+          return aliasTo(Image, child)
+        case 'input':
+          return aliasTo(Input, child)
         case 'section':
-          return <Section {...child.props}>{...child.children}</Section>
+          return aliasTo(Section, child)
+        case 'select':
+          return aliasTo(Select, child)
+        case 'textarea':
+          return aliasTo(Textarea, child)
         default:
           throw new Error(
             '<Blocks> allows only including layout block components.'
@@ -53,7 +59,7 @@ export const Blocks: JSXSlack.FC<BlocksProps> = props => {
   const node = <ArrayOutput>{normalized}</ArrayOutput>
 
   node.props[jsxOnParsed] = parsed => {
-    // Check the final output again
+    // Check the final output
     if (parsed.some(b => !knownBlocks.includes(b.type)))
       throw new Error('<Blocks> allows only including layout block components.')
 
diff --git a/src/block-kit/Input.tsx b/src/block-kit/Input.tsx
index a4ef933..a3b83fe 100644
--- a/src/block-kit/Input.tsx
+++ b/src/block-kit/Input.tsx
@@ -36,8 +36,8 @@ interface InputComponentProps extends InputCommonProps {
   minLength?: number // => PlainTextInput.minLength
 }
 
-type InputProps = InputBlockProps | InputComponentProps
-type TextareaProps = InputComponentProps
+export type InputProps = InputBlockProps | InputComponentProps
+export type TextareaProps = InputComponentProps
 
 export type WithInputProps<T> =
   | T & { [key in keyof InputCommonProps]?: undefined }
diff --git a/src/block-kit/Section.tsx b/src/block-kit/Section.tsx
index cc02d13..f66248b 100644
--- a/src/block-kit/Section.tsx
+++ b/src/block-kit/Section.tsx
@@ -1,10 +1,13 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
 /** @jsx JSXSlack.h */
 import { SectionBlock, MrkdwnElement } from '@slack/types'
 import { JSXSlack } from '../jsx'
-import { ObjectOutput } from '../utils'
+import { ObjectOutput, aliasTo } from '../utils'
 import html from '../html'
 import { BlockComponentProps } from './Blocks'
 import { Image } from './Image'
+import { Button } from './elements/Button'
+import { Select } from './elements/Select'
 
 export const Section: JSXSlack.FC<
   BlockComponentProps & { children: JSXSlack.Children<{}> }
@@ -52,6 +55,12 @@ export const Section: JSXSlack.FC<
           <Image alt={child.props.alt} src={child.props.src} />
         )
         eaten = true
+      } else if (child.type === 'button') {
+        accessory = JSXSlack(aliasTo(Button, child)!)
+        eaten = true
+      } else if (child.type === 'select') {
+        accessory = JSXSlack(aliasTo(Select, child)!)
+        eaten = true
       }
     }
 
diff --git a/src/block-kit/elements/Button.tsx b/src/block-kit/elements/Button.tsx
index 1ddf84e..2beea60 100644
--- a/src/block-kit/elements/Button.tsx
+++ b/src/block-kit/elements/Button.tsx
@@ -9,6 +9,7 @@ export interface ButtonProps {
   actionId?: string
   children: JSXSlack.Children<{}>
   confirm?: JSXSlack.Node<ConfirmProps>
+  name?: string
   style?: SlackButton['style']
   url?: string
   value?: string
@@ -18,7 +19,7 @@ export const Button: JSXSlack.FC<ButtonProps> = props => (
   <ObjectOutput<SlackButton>
     type="button"
     text={plainText(JSXSlack(<PlainText>{props.children}</PlainText>))}
-    action_id={props.actionId}
+    action_id={props.actionId || props.name}
     confirm={props.confirm ? JSXSlack(props.confirm) : undefined}
     style={props.style}
     url={props.url}
diff --git a/src/block-kit/elements/Overflow.tsx b/src/block-kit/elements/Overflow.tsx
index ba40c40..9b20869 100644
--- a/src/block-kit/elements/Overflow.tsx
+++ b/src/block-kit/elements/Overflow.tsx
@@ -13,6 +13,8 @@ export interface OverflowProps {
   // pass a <Fragment> containing multiple items.
   children: JSXSlack.Children<OverflowItemInternal>
   // children: JSXSlack.Child<OverflowItemInternal>[]
+
+  name?: string
 }
 
 interface OverflowItemProps {
@@ -40,7 +42,7 @@ export const Overflow: JSXSlack.FC<OverflowProps> = props => {
   return (
     <ObjectOutput<SlackOverflow>
       type="overflow"
-      action_id={props.actionId}
+      action_id={props.actionId || props.name}
       confirm={props.confirm ? JSXSlack(props.confirm) : undefined}
       options={opts.map(
         (o): Option => ({
diff --git a/src/block-kit/elements/Select.tsx b/src/block-kit/elements/Select.tsx
index 2308fa1..31d6dbe 100644
--- a/src/block-kit/elements/Select.tsx
+++ b/src/block-kit/elements/Select.tsx
@@ -17,12 +17,13 @@ import {
 import flattenDeep from 'lodash.flattendeep'
 import { ConfirmProps } from '../composition/Confirm'
 import { plainText } from '../composition/utils'
-import { Input, InputCommonProps, WithInputProps, wrapInInput } from '../Input'
+import { WithInputProps, wrapInInput } from '../Input'
 import { JSXSlack } from '../../jsx'
 import {
   ObjectOutput,
   PlainText,
   coerceToInteger,
+  aliasTo,
   isNode,
   wrap,
 } from '../../utils'
@@ -62,7 +63,7 @@ interface MultiSelectProps extends MultiSelectPropsBase, StaticSelectPropsBase {
   value?: string | string[]
 }
 
-type SelectProps = WithInputProps<SingleSelectProps | MultiSelectProps>
+export type SelectProps = WithInputProps<SingleSelectProps | MultiSelectProps>
 
 // External select
 interface ExternalSelectPropsBase {
@@ -134,12 +135,12 @@ type ChannelsSelectProps = WithInputProps<
 >
 
 // Options
-interface OptionProps {
+export interface OptionProps {
   value: string
   children: JSXSlack.Children<{}>
 }
 
-interface OptgroupProps {
+export interface OptgroupProps {
   label: string
   children: JSXSlack.Children<OptionInternal>
 }
@@ -157,6 +158,18 @@ type SelectFragmentObject<T extends 'options' | 'option_groups'> = Required<
   Pick<StaticSelect, T>
 >
 
+export const Option: JSXSlack.FC<OptionProps> = props => (
+  <ObjectOutput<OptionInternal>
+    {...props}
+    type="option"
+    text={JSXSlack(<PlainText>{props.children}</PlainText>)}
+  />
+)
+
+export const Optgroup: JSXSlack.FC<OptgroupProps> = props => (
+  <ObjectOutput<OptgroupInternal> {...props} type="optgroup" />
+)
+
 const baseProps = (
   props: SelectPropsBase
 ):
@@ -177,9 +190,20 @@ const createOption = ({ value, text }: OptionInternal): SlackOption => ({
 })
 
 const filter = <T extends {}>(children: JSXSlack.Children<T>) =>
-  JSXSlack.normalizeChildren(children).filter(
-    o => typeof o !== 'string'
-  ) as JSXSlack.Node<T>[]
+  JSXSlack.normalizeChildren(children).reduce(
+    (arr, n) => {
+      if (typeof n === 'string') return arr
+
+      // Convert intrinsic HTML elements
+      let e: JSXSlack.Node | undefined | null
+
+      if (n.type === 'option') e = aliasTo(Option, n)
+      if (n.type === 'optgroup') e = aliasTo(Optgroup, n)
+
+      return [...arr, e || n]
+    },
+    [] as JSXSlack.Node<T>[]
+  )
 
 const generateFragments = (
   children: SelectFragmentProps['children']
@@ -353,15 +377,3 @@ export const ChannelsSelect: JSXSlack.FC<ChannelsSelectProps> = props => {
 
   return props.label ? wrapInInput(element, props) : element
 }
-
-export const Option: JSXSlack.FC<OptionProps> = props => (
-  <ObjectOutput<OptionInternal>
-    {...props}
-    type="option"
-    text={JSXSlack(<PlainText>{props.children}</PlainText>)}
-  />
-)
-
-export const Optgroup: JSXSlack.FC<OptgroupProps> = props => (
-  <ObjectOutput<OptgroupInternal> {...props} type="optgroup" />
-)
diff --git a/src/jsx.ts b/src/jsx.ts
index f15de8a..20d2dc2 100644
--- a/src/jsx.ts
+++ b/src/jsx.ts
@@ -3,6 +3,13 @@ import flattenDeep from 'lodash.flattendeep'
 import { escapeChars, escapeEntity, parse } from './html'
 import turndown from './turndown'
 import { wrap } from './utils'
+import { InputProps, TextareaProps } from './block-kit/Input'
+import { ButtonProps } from './block-kit/elements/Button'
+import {
+  SelectProps,
+  OptionProps,
+  OptgroupProps,
+} from './block-kit/elements/Select'
 
 let internalExactMode = false
 
@@ -158,21 +165,27 @@ export namespace JSXSlack {
       b: {}
       blockquote: {}
       br: {}
+      button: Omit<ButtonProps, 'actionId'>
       code: {}
       del: {}
       em: {}
       hr: { id?: string }
       i: {}
       img: { alt: string; id?: string; src: string; title?: string }
+      input: Omit<InputProps, 'actionId' | 'blockId' | 'hint'>
       li: {}
       ol: { start?: number; children: Children<any> }
+      optgroup: OptgroupProps
+      option: OptionProps
       p: {}
       pre: {}
       s: {}
       section: { id?: string; children: Children<any> }
+      select: Omit<SelectProps, 'actionId' | 'blockId' | 'hint'>
       span: {}
       strike: {}
       strong: {}
+      textarea: Omit<TextareaProps, 'actionId' | 'blockId' | 'hint'>
       time: {
         datetime: string | number | Date
         fallback?: string
diff --git a/src/utils.tsx b/src/utils.tsx
index 2663e60..9234690 100644
--- a/src/utils.tsx
+++ b/src/utils.tsx
@@ -37,6 +37,14 @@ export function wrap<T>(children: T | T[]): T[] {
   return []
 }
 
+export const aliasTo = (component: JSXSlack.FC<any>, node: JSXSlack.Node) => {
+  return JSXSlack.h(
+    component,
+    node.props,
+    ...wrap(node.props.children || node.children)
+  )
+}
+
 export function detectSpecialLink(href: string): SpecialLink | undefined {
   if (href === '@channel') return SpecialLink.ChannelMention
   if (href === '@everyone') return SpecialLink.EveryoneMention


Test patch:
diff --git a/test/index.tsx b/test/index.tsx
index 7829b6a..5a9a956 100644
--- a/test/index.tsx
+++ b/test/index.tsx
@@ -8,6 +8,7 @@ import {
   SectionBlock,
   StaticSelect,
   Option as SlackOption,
+  Overflow as SlackOverflow,
   View,
 } from '@slack/types'
 import JSXSlack, {
@@ -219,12 +220,21 @@ describe('jsx-slack', () => {
       it('outputs section block with action accessories', () => {
         for (const accessory of [
           <Button>Button</Button>,
+          <button>button</button>,
           <Select>
             <Option value="select">Static select</Option>
           </Select>,
           <Select multiple>
             <Option value="select">Multiple select</Option>
           </Select>,
+          <select>
+            <option value="select">intrinsic &lt;select&gt;</option>
+          </select>,
+          <select multiple>
+            <optgroup label="group">
+              <option value="select">intrinsic multi-select</option>
+            </optgroup>
+          </select>,
           <ExternalSelect />,
           <ExternalSelect multiple />,
           <UsersSelect />,
@@ -442,6 +452,27 @@ describe('jsx-slack', () => {
         ).toStrictEqual([buttonAction])
       })
 
+      it('allows using HTML-compatible <button> element', () => {
+        const buttonAction = action({
+          type: 'button',
+          action_id: 'action',
+          text: { type: 'plain_text', text: 'Hello!', emoji: true },
+          value: 'value',
+        })
+
+        expect(
+          JSXSlack(
+            <Blocks>
+              <Actions blockId="actions">
+                <button name="action" value="value">
+                  Hello!
+                </button>
+              </Actions>
+            </Blocks>
+          )
+        ).toStrictEqual([buttonAction])
+      })
+
       it('outputs actions block with <Button> for link', () => {
         const buttonAction = action({
           type: 'button',
@@ -621,6 +652,33 @@ describe('jsx-slack', () => {
         ).toStrictEqual([selectAction])
       })
 
+      it('allows using HTML-compatible <select>, <option> and <optgroup> elements', () =>
+        expect(
+          JSXSlack(
+            <Blocks>
+              <Actions>
+                <select name="test">
+                  <optgroup label="foo">
+                    <option value="bar">bar</option>
+                  </optgroup>
+                </select>
+              </Actions>
+            </Blocks>
+          )
+        ).toStrictEqual(
+          JSXSlack(
+            <Blocks>
+              <Actions>
+                <Select actionId="test">
+                  <Optgroup label="foo">
+                    <Option value="bar">bar</Option>
+                  </Optgroup>
+                </Select>
+              </Actions>
+            </Blocks>
+          )
+        ))
+
       it('throws error when <Select> has not contained <Option>', () =>
         expect(() =>
           JSXSlack(
@@ -779,7 +837,7 @@ describe('jsx-slack', () => {
       })
 
       it('outputs actions block with <Overflow>', () => {
-        const overflowAction = action({
+        const baseOverflow: SlackOverflow = {
           type: 'overflow',
           action_id: 'overflow_menu',
           options: [
@@ -800,7 +858,7 @@ describe('jsx-slack', () => {
               url: 'https://example.com/',
             },
           ],
-        })
+        }
 
         expect(
           JSXSlack(
@@ -815,17 +873,65 @@ describe('jsx-slack', () => {
               </Actions>
             </Blocks>
           )
-        ).toStrictEqual([overflowAction])
+        ).toStrictEqual([action(baseOverflow)])
+
+        // confirm prop and HTML-compatible props
+        expect(
+          JSXSlack(
+            <Blocks>
+              <Actions id="actions">
+                <Overflow
+                  name="overflow_menu"
+                  confirm={
+                    <Confirm title="a" confirm="b" deny="c">
+                      foobar
+                    </Confirm>
+                  }
+                >
+                  <OverflowItem value="menu_a">Menu A</OverflowItem>
+                  <OverflowItem value="menu_b">Menu B</OverflowItem>
+                  <OverflowItem value="menu_c">Menu C</OverflowItem>
+                  <OverflowItem url="https://example.com/">Link</OverflowItem>
+                </Overflow>
+              </Actions>
+            </Blocks>
+          )
+        ).toStrictEqual([
+          action({
+            ...baseOverflow,
+            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 },
+            },
+          }),
+        ])
       })
 
+      it('throws error when <Overflow> has only one <OverflowItem>', () =>
+        expect(() =>
+          JSXSlack(
+            <Blocks>
+              <Actions>
+                <Overflow>
+                  <OverflowItem value="a">A</OverflowItem>
+                </Overflow>
+              </Actions>
+            </Blocks>
+          )
+        ).toThrow())
+
       it('throws error when <Overflow> has unexpected children', () =>
         expect(() =>
           JSXSlack(
             <Blocks>
-              <Overflow>
-                <Button>btn</Button>
-                <Button>btn</Button>
-              </Overflow>
+              <Actions>
+                <Overflow>
+                  <Button>btn</Button>
+                  <Button>btn</Button>
+                </Overflow>
+              </Actions>
             </Blocks>
           )
         ).toThrow())
@@ -1083,6 +1189,40 @@ describe('jsx-slack', () => {
               </Modal>
             ).blocks
           ).toStrictEqual(blocks)
+
+          // Intrinsic HTML elements
+          expect(
+            JSXSlack(
+              <Modal title="test">
+                <input
+                  id="input-id"
+                  label="Select"
+                  title="foobar"
+                  children={select}
+                />
+              </Modal>
+            ).blocks
+          ).toStrictEqual(blocks)
+
+          expect(
+            JSXSlack(
+              <Modal title="test">
+                <input id="input-id" label="Select" title="foobar">
+                  {select}
+                </input>
+              </Modal>
+            ).blocks
+          ).toStrictEqual(blocks)
+
+          expect(
+            JSXSlack(
+              <Modal title="test">
+                <select id="input-id" label="Select" title="foobar">
+                  <option value="test">test</option>
+                </select>
+              </Modal>
+            ).blocks
+          ).toStrictEqual(blocks)
         })
 
         it('throws error when wrapped invalid element', () => {
@@ -1184,6 +1324,17 @@ describe('jsx-slack', () => {
     })
 
     describe('<Textarea>', () => {
+      const expected: InputBlock = {
+        type: 'input',
+        label: { type: 'plain_text', text: 'textarea', emoji: true },
+        optional: true,
+        element: {
+          type: 'plain_text_input',
+          action_id: 'foobar',
+          multiline: true,
+        },
+      }
+
       it('outputs input block with plain-text input element that is enabled multiline prop', () => {
         const { blocks } = JSXSlack(
           <Modal title="test">
@@ -1191,16 +1342,15 @@ describe('jsx-slack', () => {
           </Modal>
         )
 
-        const expected: InputBlock = {
-          type: 'input',
-          label: { type: 'plain_text', text: 'textarea', emoji: true },
-          optional: true,
-          element: {
-            type: 'plain_text_input',
-            action_id: 'foobar',
-            multiline: true,
-          },
-        }
+        expect(blocks).toStrictEqual([expected])
+      })
+
+      it('allows using HTML-compatible <textarea> element', () => {
+        const { blocks } = JSXSlack(
+          <Modal title="test">
+            <textarea label="textarea" name="foobar" />
+          </Modal>
+        )
 
         expect(blocks).toStrictEqual([expected])
       })
@@ -1447,6 +1597,16 @@ describe('jsx-slack', () => {
         )
       ).toStrictEqual(expectedOptions)
 
+      expect(
+        JSXSlack(
+          <SelectFragment>
+            <option value="a">A</option>
+            <option value="b">B</option>
+            <option value="c">C</option>
+          </SelectFragment>
+        )
+      ).toStrictEqual(expectedOptions)
+
       const expectedOptgroups: Required<Pick<StaticSelect, 'option_groups'>> = {
         option_groups: [
           {
@@ -1492,6 +1652,21 @@ describe('jsx-slack', () => {
           </SelectFragment>
         )
       ).toStrictEqual(expectedOptgroups)
+
+      expect(
+        JSXSlack(
+          <SelectFragment>
+            <optgroup label="A">
+              <option value="1">one</option>
+              <option value="2">two</option>
+            </optgroup>
+            <optgroup label="B">
+              <option value="3">three</option>
+              <option value="4">four</option>
+            </optgroup>
+          </SelectFragment>
+        )
+      ).toStrictEqual(expectedOptgroups)
     })
 
     it('allows no options to return empty result', () => {