Repository: speee/jsx-slack
Pull request #63 (2019-09-30 11:23:52)

Add input-compatible props to select-like elements and DatePicker
Added props of `<Input>` to select-like elements and `<DatePicker>`, and wrap in `<Input>` automatically when using these. It allows HTML form style templating.

By this implementation, JSX for outdated dialog (`@speee-js/jsx-slack/dialog`) can migrate into modal with minimum changes in template.

```jsx
<Dialog callbackId="createUser" title="Create user">
  <UsersSelect name="user" label="User" required />
  <Select name="role" label="Role" value="regular" required>
    <Option value="regular">Regular</Option>
    <Option value="leader">Leader</Option>
    <Option value="admin">Admin</Option>
  </Select>
</Dialog>
```

In this case, just change from `Dialog` to `Modal`.

```jsx
<Modal callbackId="createUser" title="Create user">
  <UsersSelect name="user" label="User" required />
  <Select name="role" label="Role" value="regular" required>
    <Option value="regular">Regular</Option>
    <Option value="leader">Leader</Option>
    <Option value="admin">Admin</Option>
  </Select>
</Modal>
```



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d13c96d..741a56d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
 - `<Modal>` container component ([#60](https://github.com/speee/jsx-slack/pull/60))
 - `<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))
 
 ### Changed
 
diff --git a/docs/jsx-components-for-block-kit.md b/docs/jsx-components-for-block-kit.md
index 86149d2..9719406 100644
--- a/docs/jsx-components-for-block-kit.md
+++ b/docs/jsx-components-for-block-kit.md
@@ -245,7 +245,7 @@ If you want to use `<Input>` as layout block, you have to place the one of avail
 
 ```jsx
 <Modal title="My App">
-  <Input label="User" hint="Please select one of users." required>
+  <Input label="User" title="Please select one of users." required>
     <UsersSelect placeholder="Choose user..." />
   </Input>
 </Modal>
@@ -257,7 +257,7 @@ If you want to use `<Input>` as layout block, you have to place the one of avail
 
 - `label` (**required**): The label string for the element.
 - `id` / `blockId` (optional): A string of unique identifier of block.
-- `hint` (optional): Specify a helpful text appears under the element.
+- `title`/ `hint` (optional): Specify a helpful text appears under the element. `title` is alias to `hint` prop for keeping HTML compatibility.
 - `required` (optional): A boolean prop to specify whether any value must be filled when user confirms modal. `false` by default for HTML compatibility, and _notice that it is different from Slack's default._
 
 #### Available elements as a child
@@ -267,8 +267,7 @@ If you want to use `<Input>` as layout block, you have to place the one of avail
 - [`<UsersSelect>`](#usersselect-select-menu-with-user-list)
 - [`<ConversationsSelect>`](#conversationsselect-select-menu-with-conversations-list)
 - [`<ChannelsSelect>`](#channelsselect-select-menu-with-channel-list)
-
-In jsx-slack, [`<Input>` component also can use as the block element for plain text input in modal.](#input-element)
+- [`<DatePicker>`](#datepicker-select-date-from-calendar)
 
 ## Block elements
 
@@ -319,7 +318,9 @@ A menu element with static options passed by `<Option>` or `<Optgroup>`. It has
 
 [<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%22actions%22%2C%22elements%22%3A%5B%7B%22type%22%3A%22static_select%22%2C%22placeholder%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Rate%20it!%22%2C%22emoji%22%3Atrue%7D%2C%22action_id%22%3A%22rating%22%2C%22options%22%3A%5B%7B%22value%22%3A%225%22%2C%22text%22%3A%7B%22text%22%3A%225%20%3Astar%3A%3Astar%3A%3Astar%3A%3Astar%3A%3Astar%3A%22%2C%22type%22%3A%22plain_text%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22value%22%3A%224%22%2C%22text%22%3A%7B%22text%22%3A%224%20%3Astar%3A%3Astar%3A%3Astar%3A%3Astar%3A%22%2C%22type%22%3A%22plain_text%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22value%22%3A%223%22%2C%22text%22%3A%7B%22text%22%3A%223%20%3Astar%3A%3Astar%3A%3Astar%3A%22%2C%22type%22%3A%22plain_text%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22value%22%3A%222%22%2C%22text%22%3A%7B%22text%22%3A%222%20%3Astar%3A%3Astar%3A%22%2C%22type%22%3A%22plain_text%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22value%22%3A%221%22%2C%22text%22%3A%7B%22text%22%3A%221%20%3Astar%3A%22%2C%22type%22%3A%22plain_text%22%2C%22emoji%22%3Atrue%7D%7D%5D%7D%5D%7D%5D)
 
-By defining `multiple` attribute, you also can provide [the selectable menu from multiple options][multi-select] with an appearance is similar to button or text input. The same goes for other menu-like components.
+#### Multiple select
+
+By defining `multiple` attribute, you also can provide [the selectable menu from multiple options][multi-select] with an appearance is similar to button or text input. The same goes for other select-like components.
 
 [multi-select]: https://api.slack.com/reference/block-kit/block-elements#multi_select
 
@@ -346,9 +347,46 @@ By defining `multiple` attribute, you also can provide [the selectable menu from
 
 > :warning: Slack does not allow to place the multi-select menu in `Actions` block. So jsx-slack throws error if you're trying to use `multiple` attribute in the children of `<Actions>`.
 
+#### Usage in `<Modal>` container
+
+In `<Modal>` container, select-like components may place as children of `<Modal>` directly by wrapping in [`<Input>` layout block](#input-block) by passing `label` prop. Thereby it would allow natural templating like as HTML form style.
+
+```jsx
+<Modal title="My App">
+  <Select
+    label="Language"
+    name="language"
+    title="Pick language you want to learn."
+    required
+  >
+    <Option value="javascript">JavaScript</Option>
+    <Option value="python">Python</Option>
+    <Option value="java">Java</Option>
+    <Option value="c-sharp">C#</Option>
+    <Option value="php">PHP</Option>
+  </Select>
+</Modal>
+```
+
+The above JSX means exactly same as following:
+
+<!-- prettier-ignore-start -->
+
+```jsx
+<Modal title="My App">
+  <Input label="Language" title="Pick language you want to learn." required>
+    <Select actionId="language">
+      ...
+    </Select>
+  </Input>
+</Modal>
+```
+
+<!-- prettier-ignore-end -->
+
 #### Props
 
-- `actionId` (optional): An identifier for the action.
+- `name` / `actionId` (optional): An identifier for the action.
 - `placeholder` (optional): A plain text to be shown at first.
 - `value` (optional): A value of item to show initially. It must choose value from defined `<Option>` elements in children. It can pass multiple string values by array when `multiple` is enabled.
 - `confirm` (optional): [`<Confirm>` element](#confirm-confirmation-dialog) to show confirmation dialog.
@@ -358,6 +396,13 @@ By defining `multiple` attribute, you also can provide [the selectable menu from
 - `multiple` (optional): A boolean value that shows whether multiple options can be selected.
 - `maxSelectedItems` (optional): A maximum number of items to allow selected.
 
+##### Props for modal's input
+
+- `label` (**required**): The label string for the element.
+- `id` / `blockId` (optional): A string of unique identifier of [`<Input>` layout block](#input-block).
+- `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.
+
 #### `<Option>`: Menu item
 
 ##### Props
@@ -412,7 +457,7 @@ It requires setup JSON entry URL in your Slack app. [Learn about external source
 
 #### Props
 
-- `actionId` (optional): An identifier for the action.
+- `name` / `actionId` (optional): An identifier for the action.
 - `placeholder` (optional): A plain text to be shown at first.
 - `initialOption` (optional): An initial option exactly matched to provided options from external source. It allows raw JSON object or `<Option>`. It can pass multiple options by array when `multiple` is enabled.
 - `minQueryLength` (optional): A length of typed characters to begin JSON request.
@@ -423,6 +468,13 @@ It requires setup JSON entry URL in your Slack app. [Learn about external source
 - `multiple` (optional): A boolean value that shows whether multiple options can be selected.
 - `maxSelectedItems` (optional): A maximum number of items to allow selected.
 
+##### Props for modal's input
+
+- `label` (**required**): The label string for the element.
+- `id` / `blockId` (optional): A string of unique identifier of [`<Input>` layout block](#input-block).
+- `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.
+
 #### `<SelectFragment>`: Generate options for external source
 
 You would want to build not only the message but also the data source by jsx-slack. `<SelectFragment>` component can create JSON object for external data source usable in `<ExternalSelect>`.
@@ -454,7 +506,7 @@ A select menu with options consisted of users in the current workspace.
 
 #### Props
 
-- `actionId` (optional): An identifier for the action.
+- `name` / `actionId` (optional): An identifier for the action.
 - `placeholder` (optional): A plain text to be shown at first.
 - `initialUser` (optional): The initial user ID. It can pass multiple string values by array when `multiple` is enabled.
 - `confirm` (optional): [`<Confirm>` element](#confirm-confirmation-dialog) to show confirmation dialog.
@@ -464,13 +516,20 @@ A select menu with options consisted of users in the current workspace.
 - `multiple` (optional): A boolean value that shows whether multiple options can be selected.
 - `maxSelectedItems` (optional): A maximum number of items to allow selected.
 
+##### Props for modal's input
+
+- `label` (**required**): The label string for the element.
+- `id` / `blockId` (optional): A string of unique identifier of [`<Input>` layout block](#input-block).
+- `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.
+
 ### [`<ConversationsSelect>`: Select menu with conversations list](https://api.slack.com/reference/messaging/block-elements#conversation-select)
 
 A select menu with options consisted of any type of conversations in the current workspace.
 
 #### Props
 
-- `actionId` (optional): An identifier for the action.
+- `name` / `actionId` (optional): An identifier for the action.
 - `placeholder` (optional): A plain text to be shown at first.
 - `initialConversation` (optional): The initial conversation ID. It can pass multiple string values by array when `multiple` is enabled.
 - `confirm` (optional): [`<Confirm>` element](#confirm-confirmation-dialog) to show confirmation dialog.
@@ -480,13 +539,20 @@ A select menu with options consisted of any type of conversations in the current
 - `multiple` (optional): A boolean value that shows whether multiple options can be selected.
 - `maxSelectedItems` (optional): A maximum number of items to allow selected.
 
+##### Props for modal's input
+
+- `label` (**required**): The label string for the element.
+- `id` / `blockId` (optional): A string of unique identifier of [`<Input>` layout block](#input-block).
+- `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.
+
 ### [`<ChannelsSelect>`: Select menu with channel list](https://api.slack.com/reference/messaging/block-elements#channel-select)
 
 A select menu with options consisted of public channels in the current workspace.
 
 #### Props
 
-- `actionId` (optional): An identifier for the action.
+- `name` / `actionId` (optional): An identifier for the action.
 - `placeholder` (optional): A plain text to be shown at first.
 - `initialChannel` (optional): The initial channel ID. It can pass multiple string values by array when `multiple` is enabled.
 - `confirm` (optional): [`<Confirm>` element](#confirm-confirmation-dialog) to show confirmation dialog.
@@ -496,6 +562,13 @@ A select menu with options consisted of public channels in the current workspace
 - `multiple` (optional): A boolean value that shows whether multiple options can be selected.
 - `maxSelectedItems` (optional): A maximum number of items to allow selected.
 
+##### Props for modal's input
+
+- `label` (**required**): The label string for the element.
+- `id` / `blockId` (optional): A string of unique identifier of [`<Input>` layout block](#input-block).
+- `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.
+
 ### [`<Overflow>`: Overflow menu](https://api.slack.com/reference/messaging/block-elements#overflow)
 
 An overflow menu displayed as `...` can access to some hidden menu items by many actions. _It must contain least of 2 `<OverflowItem>` components._
@@ -542,11 +615,28 @@ An easy way to let the user selecting any date is using `<DatePicker>` component
 
 #### Props
 
-- `actionId` (optional): An identifier for the action.
+- `name` / `actionId` (optional): An identifier for the action.
 - `placeholder` (optional): A plain text to be shown at first.
 - `initialDate` (optional): An initially selected date. It allows `YYYY-MM-DD` formatted string, UNIX timestamp in millisecond, and JavaScript [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) instance.
 - `confirm` (optional): [`<Confirm>` element](#confirm-confirmation-dialog) to show confirmation dialog.
 
+##### Props for modal's input
+
+As same as [select-like elements](#usage-in-modal-container), `<DatePicker>` also can place as children of `<Modal>` by passing required props.
+
+```jsx
+<Modal title="My App">
+  <DatePicker label="Date" name="date" />
+</Modal>
+```
+
+[<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%22Date%22%2C%22emoji%22%3Atrue%7D%2C%22optional%22%3Atrue%2C%22element%22%3A%7B%22type%22%3A%22datepicker%22%2C%22action_id%22%3A%22date%22%7D%7D%5D&mode=modal)
+
+- `label` (**required**): The label string for the element.
+- `id` / `blockId` (optional): A string of unique identifier of [`<Input>` layout block](#input-block).
+- `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.
+
 ### [`<Input>`: Plain-text input element](https://api.slack.com/reference/block-kit/block-elements#input) <a name="input-element" id="input-element">(Only for modal)</a>
 
 `<Input>` block element is for placing a single-line input form within `<Modal>`. It has a interface similar to `<input>` HTML element.
diff --git a/src/block-kit/Actions.tsx b/src/block-kit/Actions.tsx
index 1a9b5b2..2260ec7 100644
--- a/src/block-kit/Actions.tsx
+++ b/src/block-kit/Actions.tsx
@@ -6,11 +6,11 @@ import { BlockComponentProps } from './Blocks'
 import { ButtonProps } from './elements/Button'
 import { SingleSelectPropsBase } from './elements/Select'
 import { OverflowProps } from './elements/Overflow'
-import { DatePickerProps } from './elements/DatePicker'
+import { DatePickerBaseProps } from './elements/DatePicker'
 
 interface ActionsProps extends BlockComponentProps {
   children: JSXSlack.Children<
-    ButtonProps | SingleSelectPropsBase | OverflowProps | DatePickerProps
+    ButtonProps | SingleSelectPropsBase | OverflowProps | DatePickerBaseProps
   >
 }
 
diff --git a/src/block-kit/Input.tsx b/src/block-kit/Input.tsx
index dc045ef..286538a 100644
--- a/src/block-kit/Input.tsx
+++ b/src/block-kit/Input.tsx
@@ -6,8 +6,9 @@ import { BlockComponentProps } from './Blocks'
 import { plainText } from './composition/utils'
 import { PlainTextInput } from './elements/PlainTextInput'
 
-interface InputCommonProps extends BlockComponentProps {
+export interface InputCommonProps extends BlockComponentProps {
   hint?: string
+  title?: string
   label: string
   required?: boolean
 }
@@ -16,7 +17,6 @@ interface InputBlockProps extends InputCommonProps {
   children: JSXSlack.Node<{}>
 
   // Disallow defining attributes for component usage
-  title?: undefined
   actionId?: undefined
   name?: undefined
   placeholder?: undefined
@@ -28,7 +28,6 @@ interface InputBlockProps extends InputCommonProps {
 interface InputComponentProps extends InputCommonProps {
   children?: undefined
 
-  title?: string // => InputBlockProps.hint (Alias)
   actionId?: string // => PlainTextInput.actionId
   name?: string // => PlainTextInput.actionId (Alias)
   placeholder?: string // => PlainTextInput.placeholder
@@ -40,16 +39,29 @@ interface InputComponentProps extends InputCommonProps {
 type InputProps = InputBlockProps | InputComponentProps
 type TextareaProps = InputComponentProps
 
-const InputComponent: JSXSlack.FC<
-  InputComponentProps & { multiline?: boolean }
-> = props => (
+export type WithInputProps<T> =
+  | T & { [key in keyof InputCommonProps]?: undefined }
+  | T & InputCommonProps
+
+export const wrapInInput = (
+  element: JSXSlack.Node<any>,
+  props: InputCommonProps
+) => (
   <Input
     blockId={props.blockId}
-    hint={props.hint || props.title}
+    children={element}
+    hint={props.hint}
     id={props.id}
     label={props.label}
     required={props.required}
-  >
+    title={props.title}
+  />
+)
+
+const InputComponent: JSXSlack.FC<
+  InputComponentProps & { multiline?: boolean }
+> = props =>
+  wrapInInput(
     <PlainTextInput
       actionId={props.actionId || props.name}
       initialValue={props.value}
@@ -57,18 +69,20 @@ const InputComponent: JSXSlack.FC<
       minLength={coerceToInteger(props.minLength)}
       multiline={props.multiline}
       placeholder={props.placeholder}
-    />
-  </Input>
-)
+    />,
+    props
+  )
 
 export const Input: JSXSlack.FC<InputProps> = props => {
   if (props.children === undefined) return InputComponent(props)
 
+  const hintText = props.hint || props.title
+
   return (
     <ObjectOutput<InputBlock>
       type="input"
       block_id={props.id || props.blockId}
-      hint={props.hint ? plainText(props.hint) : undefined}
+      hint={hintText ? plainText(hintText) : undefined}
       label={plainText(props.label)}
       optional={!props.required}
       element={JSXSlack(props.children)}
diff --git a/src/block-kit/elements/DatePicker.tsx b/src/block-kit/elements/DatePicker.tsx
index 8fd7b83..e770316 100644
--- a/src/block-kit/elements/DatePicker.tsx
+++ b/src/block-kit/elements/DatePicker.tsx
@@ -4,15 +4,19 @@ import { JSXSlack } from '../../jsx'
 import { ObjectOutput } from '../../utils'
 import { ConfirmProps } from '../composition/Confirm'
 import { plainText } from '../composition/utils'
+import { WithInputProps, wrapInInput } from '../Input'
 
-export interface DatePickerProps {
+export interface DatePickerBaseProps {
   actionId?: string
   children?: undefined
   confirm?: JSXSlack.Node<ConfirmProps>
   initialDate?: string | Date
+  name?: string
   placeholder?: string
 }
 
+type DatePickerProps = WithInputProps<DatePickerBaseProps>
+
 const formatYMD = (date: Date) =>
   [
     `${date.getFullYear()}`.padStart(4, '0'),
@@ -20,16 +24,20 @@ const formatYMD = (date: Date) =>
     `${date.getDate()}`.padStart(2, '0'),
   ].join('-')
 
-export const DatePicker: JSXSlack.FC<DatePickerProps> = props => (
-  <ObjectOutput<Datepicker>
-    type="datepicker"
-    action_id={props.actionId}
-    confirm={props.confirm ? JSXSlack(props.confirm) : undefined}
-    placeholder={props.placeholder ? plainText(props.placeholder) : undefined}
-    initial_date={
-      props.initialDate instanceof Date
-        ? formatYMD(props.initialDate)
-        : props.initialDate
-    }
-  />
-)
+export const DatePicker: JSXSlack.FC<DatePickerProps> = props => {
+  const element = (
+    <ObjectOutput<Datepicker>
+      type="datepicker"
+      action_id={props.actionId || props.name}
+      confirm={props.confirm ? JSXSlack(props.confirm) : undefined}
+      placeholder={props.placeholder ? plainText(props.placeholder) : undefined}
+      initial_date={
+        props.initialDate instanceof Date
+          ? formatYMD(props.initialDate)
+          : props.initialDate
+      }
+    />
+  )
+
+  return props.label ? wrapInInput(element, props) : element
+}
diff --git a/src/block-kit/elements/Select.tsx b/src/block-kit/elements/Select.tsx
index a9d8ac5..2308fa1 100644
--- a/src/block-kit/elements/Select.tsx
+++ b/src/block-kit/elements/Select.tsx
@@ -17,6 +17,7 @@ import {
 import flattenDeep from 'lodash.flattendeep'
 import { ConfirmProps } from '../composition/Confirm'
 import { plainText } from '../composition/utils'
+import { Input, InputCommonProps, WithInputProps, wrapInInput } from '../Input'
 import { JSXSlack } from '../../jsx'
 import {
   ObjectOutput,
@@ -31,6 +32,7 @@ export interface SingleSelectPropsBase {
   confirm?: JSXSlack.Node<ConfirmProps>
   maxSelectedItems?: undefined
   multiple?: false
+  name?: string
   placeholder?: string
 }
 
@@ -60,7 +62,7 @@ interface MultiSelectProps extends MultiSelectPropsBase, StaticSelectPropsBase {
   value?: string | string[]
 }
 
-type SelectProps = SingleSelectProps | MultiSelectProps
+type SelectProps = WithInputProps<SingleSelectProps | MultiSelectProps>
 
 // External select
 interface ExternalSelectPropsBase {
@@ -82,7 +84,9 @@ interface MultiExternalSelectProps
   initialOption?: ExternalSelectInitialOption | ExternalSelectInitialOption[]
 }
 
-type ExternalSelectProps = SingleExternalSelectProps | MultiExternalSelectProps
+type ExternalSelectProps = WithInputProps<
+  SingleExternalSelectProps | MultiExternalSelectProps
+>
 
 // Users select
 interface SingleUsersSelectProps extends SingleSelectPropsBase {
@@ -95,7 +99,9 @@ interface MultiUsersSelectProps extends MultiSelectPropsBase {
   children?: undefined
 }
 
-type UsersSelectProps = SingleUsersSelectProps | MultiUsersSelectProps
+type UsersSelectProps = WithInputProps<
+  SingleUsersSelectProps | MultiUsersSelectProps
+>
 
 // Conversations select
 interface SingleConversationsSelectProps extends SingleSelectPropsBase {
@@ -108,9 +114,9 @@ interface MultiConversationsSelectProps extends MultiSelectPropsBase {
   children?: undefined
 }
 
-type ConversationsSelectProps =
-  | SingleConversationsSelectProps
-  | MultiConversationsSelectProps
+type ConversationsSelectProps = WithInputProps<
+  SingleConversationsSelectProps | MultiConversationsSelectProps
+>
 
 // Channels select
 interface SingleChannelsSelectProps extends SingleSelectPropsBase {
@@ -123,7 +129,9 @@ interface MultiChannelsSelectProps extends MultiSelectPropsBase {
   children?: undefined
 }
 
-type ChannelsSelectProps = SingleChannelsSelectProps | MultiChannelsSelectProps
+type ChannelsSelectProps = WithInputProps<
+  SingleChannelsSelectProps | MultiChannelsSelectProps
+>
 
 // Options
 interface OptionProps {
@@ -157,9 +165,9 @@ const baseProps = (
       MultiSelect,
       'action_id' | 'confirm' | 'placeholder' | 'max_selected_items'
     > => ({
-  action_id: props.actionId,
+  action_id: props.actionId || props.name,
   confirm: props.confirm ? JSXSlack(props.confirm) : undefined,
-  max_selected_items: props.maxSelectedItems,
+  max_selected_items: coerceToInteger(props.maxSelectedItems),
   placeholder: props.placeholder ? plainText(props.placeholder) : undefined,
 })
 
@@ -239,18 +247,15 @@ export const Select: JSXSlack.FC<SelectProps> = props => {
     if (found) initialOptions.push(found)
   }
 
-  if (props.multiple) {
-    return (
-      <ObjectOutput<MultiStaticSelect>
-        type="multi_static_select"
-        {...baseProps(props)}
-        initial_options={initialOptions.length > 0 ? initialOptions : undefined}
-        {...fragment}
-      />
-    )
-  }
-
-  return (
+  // Generate element
+  const element = props.multiple ? (
+    <ObjectOutput<MultiStaticSelect>
+      type="multi_static_select"
+      {...baseProps(props)}
+      initial_options={initialOptions.length > 0 ? initialOptions : undefined}
+      {...fragment}
+    />
+  ) : (
     <ObjectOutput<StaticSelect>
       type="static_select"
       {...baseProps(props)}
@@ -258,6 +263,8 @@ export const Select: JSXSlack.FC<SelectProps> = props => {
       {...fragment}
     />
   )
+
+  return props.label ? wrapInInput(element, props) : element
 }
 
 export const ExternalSelect: JSXSlack.FC<ExternalSelectProps> = props => {
@@ -268,18 +275,14 @@ export const ExternalSelect: JSXSlack.FC<ExternalSelectProps> = props => {
       )
     : []
 
-  if (props.multiple) {
-    return (
-      <ObjectOutput<MultiExternalSelect>
-        type="multi_external_select"
-        {...baseProps(props)}
-        initial_options={initialOptions.length > 0 ? initialOptions : undefined}
-        min_query_length={minQueryLength}
-      />
-    )
-  }
-
-  return (
+  const element = props.multiple ? (
+    <ObjectOutput<MultiExternalSelect>
+      type="multi_external_select"
+      {...baseProps(props)}
+      initial_options={initialOptions.length > 0 ? initialOptions : undefined}
+      min_query_length={minQueryLength}
+    />
+  ) : (
     <ObjectOutput<SlackExternalSelect>
       type="external_select"
       {...baseProps(props)}
@@ -287,10 +290,12 @@ export const ExternalSelect: JSXSlack.FC<ExternalSelectProps> = props => {
       min_query_length={minQueryLength}
     />
   )
+
+  return props.label ? wrapInInput(element, props) : element
 }
 
-export const UsersSelect: JSXSlack.FC<UsersSelectProps> = props =>
-  props.multiple ? (
+export const UsersSelect: JSXSlack.FC<UsersSelectProps> = props => {
+  const element = props.multiple ? (
     <ObjectOutput<MultiUsersSelect>
       type="multi_users_select"
       {...baseProps(props)}
@@ -304,10 +309,13 @@ export const UsersSelect: JSXSlack.FC<UsersSelectProps> = props =>
     />
   )
 
+  return props.label ? wrapInInput(element, props) : element
+}
+
 export const ConversationsSelect: JSXSlack.FC<
   ConversationsSelectProps
-> = props =>
-  props.multiple ? (
+> = props => {
+  const element = props.multiple ? (
     <ObjectOutput<MultiConversationsSelect>
       type="multi_conversations_select"
       {...baseProps(props)}
@@ -323,8 +331,11 @@ export const ConversationsSelect: JSXSlack.FC<
     />
   )
 
-export const ChannelsSelect: JSXSlack.FC<ChannelsSelectProps> = props =>
-  props.multiple ? (
+  return props.label ? wrapInInput(element, props) : element
+}
+
+export const ChannelsSelect: JSXSlack.FC<ChannelsSelectProps> = props => {
+  const element = props.multiple ? (
     <ObjectOutput<MultiChannelsSelect>
       type="multi_channels_select"
       {...baseProps(props)}
@@ -340,6 +351,9 @@ export const ChannelsSelect: JSXSlack.FC<ChannelsSelectProps> = props =>
     />
   )
 
+  return props.label ? wrapInInput(element, props) : element
+}
+
 export const Option: JSXSlack.FC<OptionProps> = props => (
   <ObjectOutput<OptionInternal>
     {...props}


Test patch:
diff --git a/test/index.tsx b/test/index.tsx
index 1272444..204b9a1 100644
--- a/test/index.tsx
+++ b/test/index.tsx
@@ -120,7 +120,7 @@ describe('jsx-slack', () => {
     })
   })
 
-  describe('Block Kit components', () => {
+  describe('Layout blocks', () => {
     describe('<Section>', () => {
       const section: SectionBlock = {
         type: 'section',
@@ -1034,13 +1034,27 @@ describe('jsx-slack', () => {
 
           const { blocks } = JSXSlack(
             <Modal title="test">
-              <Input id="input-id" label="Select" hint="foobar">
+              <Input blockId="input-id" label="Select" hint="foobar">
                 {select}
               </Input>
             </Modal>
           )
 
           expect(blocks).toStrictEqual([expected])
+
+          // HTML-compatible aliases
+          expect(
+            JSXSlack(
+              <Modal title="test">
+                <Input
+                  id="input-id"
+                  label="Select"
+                  title="foobar"
+                  children={select}
+                />
+              </Modal>
+            ).blocks
+          ).toStrictEqual(blocks)
         })
       })
 
@@ -1090,6 +1104,41 @@ describe('jsx-slack', () => {
         })
       })
     })
+  })
+
+  describe('Block elements', () => {
+    describe('Input-compatible components', () => {
+      it('wraps element for select in <Input> block when passed label prop', () => {
+        for (const Selectable of [
+          props => (
+            <Select {...props}>
+              <Option value="test">test</Option>
+            </Select>
+          ),
+          ExternalSelect,
+          ChannelsSelect,
+          ConversationsSelect,
+          UsersSelect,
+          DatePicker,
+        ]) {
+          expect(
+            JSXSlack(
+              <Modal title="test">
+                <Selectable id="test" label="Test" name="name" title="foobar" />
+              </Modal>
+            )
+          ).toStrictEqual(
+            JSXSlack(
+              <Modal title="test">
+                <Input blockId="test" label="Test" hint="foobar">
+                  <Selectable actionId="name" />
+                </Input>
+              </Modal>
+            )
+          )
+        }
+      })
+    })
 
     describe('<Textarea>', () => {
       it('outputs input block with plain-text input element that is enabled multiline prop', () => {