Repository: speee/jsx-slack (ts)

Issue:
<Header> block
[`header` layout block](https://api.slack.com/reference/block-kit/blocks#header) is coming to Slack! Now we can get a big and bold text content through using `header` block.

```jsx
<Blocks>
  <Header>
    Heads up!
  </Header>
</Blocks>
```

![header](https://user-images.githubusercontent.com/3993388/88868881-4ed2b280-d24c-11ea-824a-59f71c042ec2.png)

> :warning: Currently it does not supported mrkdwn, just only plain text.

### ToDo

- [x] Add `<Header>` component and `<header>` alias (intrinsic HTML)
- [x] Change the example of custom block on docs
  - [Custom-defined `<Header>` will conflict with added component.](https://github.com/speee/jsx-slack#fragments)


Pull request description:
Add <Header> component for layout block
The added `<Header>` component generates [`header` layout block](https://api.slack.com/reference/messaging/blocks#header) to display plain text with bold font in a larger.

```jsx
<Blocks>
  <Header>Heads up!</Header>
</Blocks>
```

By following our customary, the same name HTML intrinsic element `<header>` also can use as alias to `<Header>`.

```jsx
<Home>
  <header id="header">Heads up!</header>
</Home>
```

It only allows `plain_text` and is not allowed `mrkdwn` for now. Thus, we will ignore some styling tags in `<Header>`.

We're generating the plain text element through an internal helper that is already using in such as `<Button>`. An important difference is that `<Header>` accepts `<br />` tags for inserting line breaks.

Closes #184.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8bc514a..983bb98 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## [Unreleased]
 
+### Added
+
+- [`<Header>` component](https://github.com/speee/jsx-slack/blob/master/docs/layout-blocks.md#header) for layout block ([#184](https://github.com/speee/jsx-slack/issues/184), [#185](https://github.com/speee/jsx-slack/pull/185))
+
 ## v2.3.0 - 2020-07-22
 
 ### Added
diff --git a/README.md b/README.md
index 75d5d8e..f61767a 100644
--- a/README.md
+++ b/README.md
@@ -204,9 +204,7 @@ By using jsx-slack, you can build a template with piling up Block Kit blocks by
 ```jsx
 <Home>
   <Image src="https://source.unsplash.com/random/960x240?home" alt="home" />
-  <Section>
-    <b>Welcome back to my home!</b> :house_with_garden:
-  </Section>
+  <Header>Welcome back to my home! :house_with_garden:</Header>
   <Divider />
   <Section>What's next?</Section>
   <Actions>
@@ -289,13 +287,13 @@ Ported from templates for [Block Kit Builder].
 
 For example, you can define the custom block by grouping some blocks with `<Fragment>` if you were using JSX transpiler.
 
-Let's say defines `<Header>` custom block that is consisted by `<Section>` and `<Divider>`.
+Let's say about defining `<Heading>` custom block that is consisted by `<Section>` and `<Divider>`.
 
 ```javascript
 /** @jsx JSXSlack.h */
 import { JSXSlack, Fragment } from '@speee-js/jsx-slack'
 
-const Header = ({ children }) => (
+const Heading = ({ children }) => (
   <Fragment>
     <Section>
       <b>{children}</b>
@@ -309,9 +307,9 @@ Now the defined block can use in `<Blocks>` as like as the other blocks:
 
 ```jsx
 <Blocks>
-  <Header>
+  <Heading>
     <i>jsx-slack custom block</i> :sunglasses:
-  </Header>
+  </Heading>
   <Section>Let's build your block.</Section>
 </Blocks>
 ```
diff --git a/demo/examples/app-home.js b/demo/examples/app-home.js
index 24004f2..a4ed8e2 100644
--- a/demo/examples/app-home.js
+++ b/demo/examples/app-home.js
@@ -1,9 +1,9 @@
 export const appHomeProjectTracker = `
 <!-- Ported from Slack Block Kit Builder template -->
 <Home>
-  <Section>
-    <b>Here's what you can do with Project Tracker:</b>
-  </Section>
+  <Header>
+    Here's what you can do with Project Tracker:
+  </Header>
   <Actions>
     <Button value="create_task" style="primary">Create New Task</Button>
     <Button value="create_project">Create New Project</Button>
diff --git a/demo/examples/basic.js b/demo/examples/basic.js
index 4a84372..446c4a0 100644
--- a/demo/examples/basic.js
+++ b/demo/examples/basic.js
@@ -55,9 +55,7 @@ export const modal = `
 export const home = `
 <Home>
   <Image src="https://source.unsplash.com/random/960x240?home" alt="home" />
-  <Section>
-    <b>Welcome back to my home!</b> :house_with_garden:
-  </Section>
+  <Header>Welcome back to my home! :house_with_garden:</Header>
   <Divider />
   <Section>What's next?</Section>
   <Actions>
diff --git a/demo/examples/messaging.js b/demo/examples/messaging.js
index f45d948..83f1c36 100644
--- a/demo/examples/messaging.js
+++ b/demo/examples/messaging.js
@@ -349,9 +349,9 @@ export const messagingSearchResultsFileCard = `
 export const messagingNewsletter = `
 <!-- Ported from Slack Block Kit Builder template -->
 <Blocks>
-  <Section>
-    :newspaper:&nbsp;&nbsp;<b>Paper Company Newsletter</b>&nbsp;&nbsp;:newspaper:
-  </Section>
+  <Header>
+    :newspaper:&nbsp;&nbsp;Paper Company Newsletter&nbsp;&nbsp;:newspaper:
+  </Header>
   <Context>
     <b>November 12, 2019</b>&nbsp;&nbsp;|&nbsp;&nbsp;Sales Team Announcements
   </Context>
diff --git a/demo/examples/modal.js b/demo/examples/modal.js
index 8fd056d..9e85943 100644
--- a/demo/examples/modal.js
+++ b/demo/examples/modal.js
@@ -169,7 +169,7 @@ export const modalSettingsNotification = `
 export const modalListOfInformationYourItinerary = `
 <!-- Ported from Slack Block Kit Builder template -->
 <Modal title="Your itinerary" close="Cancel">
-  <Section>:tada: You're all set! This is your booking summary.</Section>
+  <Header>:tada: You're all set! This is your booking summary.</Header>
   <Divider />
   <Section>
     <Field>
diff --git a/demo/schema.js b/demo/schema.js
index a7d9ae2..38431f2 100644
--- a/demo/schema.js
+++ b/demo/schema.js
@@ -62,11 +62,13 @@ const commonKnownBlocks = [
   'Image',
   'Actions',
   'Context',
+  'Header',
 
   // HTML compatible
   'section',
   'hr',
   'img',
+  'header',
 ]
 
 const schema = {
@@ -156,6 +158,8 @@ const schema = {
     attrs: blockCommonAttrs,
     children: ['Image', 'Mrkdwn', 'img', 'span', ...markupHTML],
   },
+  Header: { attrs: blockCommonAttrs, children: ['br'] },
+  header: { id: null, children: ['br'] },
   File: {
     attrs: { externalId: null, source: ['remote'], ...blockCommonAttrs },
     children: [],
diff --git a/docs/how-to-setup-jsx-transpiler.md b/docs/how-to-setup-jsx-transpiler.md
index 94dbca0..48b3352 100644
--- a/docs/how-to-setup-jsx-transpiler.md
+++ b/docs/how-to-setup-jsx-transpiler.md
@@ -30,7 +30,7 @@ module.exports = (api) => ({
 You should always import `JSXSlack` from `@speee-js/jsx-slack` in every JSX.
 
 ```jsx
-const { JSXSlack, Blocks, Section } = require('@speee-js/jsx-slack')
+const { JSXSlack, Blocks, Section } = require('@speee-js/js-slack')
 
 console.log(
   <Blocks>
@@ -48,7 +48,7 @@ You can also use comment pragma per JSX file if you have already set up JSX tran
 ```jsx
 /** @jsx JSXSlack.h **/
 /** @jsxFrag JSXSlack.Fragment **/
-const { JSXSlack } = require('@speee-js/jsx-slack')
+const { JSXSlack } = require('@speee-js/js-slack')
 ```
 
 ### Automatic ([Babel >= 7.9](https://babeljs.io/blog/2020/03/16/7.9.0#a-new-jsx-transform-11154-https-githubcom-babel-babel-pull-11154)) _[experimental]_
@@ -74,7 +74,7 @@ module.exports = (api) => ({
 Babel will automatically import functions for transpiling JSX. You only have to import required components from `@speee-js/jsx-slack`.
 
 ```jsx
-const { Blocks, Section } = require('@speee-js/jsx-slack')
+const { Blocks, Section } = require('@speee-js/js-slack')
 
 console.log(
   <Blocks>
@@ -111,7 +111,7 @@ You should always import `JSXSlack` from `@speee-js/jsx-slack` in every JSX.
 In addition, we recommend to wrap JSX in `JSXSlack()` to deal with the mismatched type against SDK for Slack API. It's a helper function to cast into `any` type.
 
 ```jsx
-import { JSXSlack, Blocks, Section } from '@speee-js/jsx-slack'
+import { JSXSlack, Blocks, Section } from '@speee-js/js-slack'
 
 console.log(
   JSXSlack(
@@ -128,7 +128,7 @@ console.log(
 
 ```jsx
 /** @jsx JSXSlack.h **/
-import { JSXSlack } from '@speee-js/jsx-slack'
+import { JSXSlack } from '@speee-js/js-slack'
 ```
 
 > :warning: TypeScript cannot customize the factory method for fragment short syntax `<></>`. ([Microsoft/TypeScript#20469](https://github.com/Microsoft/TypeScript/issues/20469)) Please use `<Fragment>` component as same as other components.
diff --git a/docs/jsx-components-for-block-kit.md b/docs/jsx-components-for-block-kit.md
index 90476f6..88acc52 100644
--- a/docs/jsx-components-for-block-kit.md
+++ b/docs/jsx-components-for-block-kit.md
@@ -14,6 +14,7 @@
   - [**`<Field>`**: Fields for section block](layout-blocks.md#field)
 - [**`<Divider>`**: Divider Block](layout-blocks.md#divider)
 - [**`<Image>`**: Image Block](layout-blocks.md#image)
+- [**`<Header>`**: Header Block](layout-blocks.md#header)
 - [**`<Actions>`**: Actions Block](layout-blocks.md#actions)
 - [**`<Context>`**: Context Block](layout-blocks.md#context)
 - [**`<File>`**: File Block](layout-blocks.md#file) (Only for messaging)
diff --git a/docs/layout-blocks.md b/docs/layout-blocks.md
index 87a29e2..b619b5f 100644
--- a/docs/layout-blocks.md
+++ b/docs/layout-blocks.md
@@ -122,6 +122,24 @@ Display an image block. It has well-known props like `<img>` HTML element. In fa
 - `title` (optional): An optional title for the image.
 - `id` / `blockId` (optional): A string of unique identifier of block.
 
+## <a name="header" id="header"></a> [`<Header>`: Header Block](https://api.slack.com/reference/messaging/blocks#header)
+
+Display a plain text in a larger and bold font. The same name `<header>` intrinsic HTML element works as well.
+
+```jsx
+<Blocks>
+  <Header>Heads up!</Header>
+</Blocks>
+```
+
+[<img src="./preview-btn.svg" width="240" />](https://speee-jsx-slack.netlify.app/#bkb:jsx:eJyzccrJT84utuNSULDxSE1MSS2yA1HFCqUFijb6UBEuG32oMgBi2w70)
+
+Please be aware that header layout block _only accepts the plain text_ for now. `<br />` will work but [most other HTML-like elements for styling](./html-like-formatting.md#basic-text-formatting) would not work.
+
+### Props
+
+- `id` / `blockId` (optional): A string of unique identifier of block.
+
 ## <a name="actions" id="actions"></a> [`<Actions>`: Actions Block](https://api.slack.com/reference/messaging/blocks#actions)
 
 A block to hold [interactive components](block-elements.md#interactive-components) provided by block elements. Slack allows a maximum of 25 interactive elements in `<Actions>` (But recommends to place up to 5 elements).
diff --git a/src/block-kit/composition/utils.ts b/src/block-kit/composition/utils.ts
index 7c46416..5a94265 100644
--- a/src/block-kit/composition/utils.ts
+++ b/src/block-kit/composition/utils.ts
@@ -36,23 +36,37 @@ export interface FilterProps {
 }
 
 // Text composition object for plain text
-const renderAsPlainText = (children: JSXSlack.ChildElements) =>
+const renderAsPlainText = (
+  children: JSXSlack.ChildElements,
+  layoutTags = false
+) =>
   JSXSlack.Children.toArray(children)
-    .map((child) =>
-      JSXSlack.isValidElement(child)
-        ? renderAsPlainText(child.$$jsxslack.children)
-        : child
-    )
+    .map((child) => {
+      if (!JSXSlack.isValidElement(child)) return child
+
+      if (layoutTags) {
+        if (child.$$jsxslack.type === 'br') return '\n'
+        // TODO: Support pargraphs by <p> tag
+      }
+
+      return renderAsPlainText(child.$$jsxslack.children)
+    })
     .join('')
 
 export const plainText = (
   textOrElements: JSXSlack.ChildElements,
-  { emoji }: Omit<PlainTextElement, 'type' | 'text'> = { emoji: true }
+  {
+    emoji = true,
+    layoutTags = false,
+  }: {
+    emoji?: boolean
+    layoutTags?: boolean
+  } = {}
 ): PlainTextElement => {
   const text =
     typeof textOrElements === 'string'
       ? textOrElements
-      : renderAsPlainText(textOrElements)
+      : renderAsPlainText(textOrElements, layoutTags)
 
   return { type: 'plain_text', text, emoji }
 }
diff --git a/src/block-kit/container/Blocks.ts b/src/block-kit/container/Blocks.ts
index 325f603..6207a49 100644
--- a/src/block-kit/container/Blocks.ts
+++ b/src/block-kit/container/Blocks.ts
@@ -1,5 +1,6 @@
 import { availableActionTypes } from '../layout/Actions'
 import { Divider } from '../layout/Divider'
+import { Header } from '../layout/Header'
 import { Image } from '../layout/Image'
 import { availableSectionAccessoryTypes, Section } from '../layout/Section'
 import {
@@ -20,6 +21,7 @@ const blockTypeFilter = (t) => t !== 'radio_buttons' && t !== 'checkboxes'
  * - `<Section>` (`<section>`)
  * - `<Image>` (`<img>`)
  * - `<Divider>` (`<hr>`)
+ * - `<Header>` (`<header>`)
  * - `<Context>`
  * - `<Actions>`
  * - `<File>`
@@ -53,12 +55,14 @@ export const Blocks = generateBlocksContainer({
     context: true,
     divider: true,
     file: true,
+    header: true,
     image: true,
     section: generateSectionValidator(
       availableSectionAccessoryTypes.filter(blockTypeFilter)
     ),
   },
   aliases: {
+    header: Header,
     hr: Divider,
     img: Image,
     section: Section,
diff --git a/src/block-kit/container/Home.tsx b/src/block-kit/container/Home.tsx
index 3b192f2..763a207 100644
--- a/src/block-kit/container/Home.tsx
+++ b/src/block-kit/container/Home.tsx
@@ -7,6 +7,7 @@ import {
   createElementInternal,
 } from '../../jsx'
 import { Divider } from '../layout/Divider'
+import { Header } from '../layout/Header'
 import { Image } from '../layout/Image'
 import { Input } from '../layout/Input'
 import { Section } from '../layout/Section'
@@ -67,10 +68,11 @@ const HomeBlocks = generateBlocksContainer({
     actions: generateActionsValidator(),
     context: true,
     divider: true,
+    header: true,
     image: true,
     section: generateSectionValidator(),
   },
-  aliases: { hr: Divider, img: Image, section: Section },
+  aliases: { header: Header, hr: Divider, img: Image, section: Section },
 })
 
 /**
@@ -82,6 +84,7 @@ const HomeBlocks = generateBlocksContainer({
  * - `<Section>` (`<section>`)
  * - `<Image>` (`<img>`)
  * - `<Divider>` (`<hr>`)
+ * - `<Header>` (`<header>`)
  * - `<Context>`
  * - `<Actions>`
  *
diff --git a/src/block-kit/container/Modal.tsx b/src/block-kit/container/Modal.tsx
index 246432b..7c7657d 100644
--- a/src/block-kit/container/Modal.tsx
+++ b/src/block-kit/container/Modal.tsx
@@ -11,6 +11,7 @@ import { plainText } from '../composition/utils'
 import { Select } from '../elements/Select'
 import { Textarea } from '../input/Textarea'
 import { Divider } from '../layout/Divider'
+import { Header } from '../layout/Header'
 import { Image } from '../layout/Image'
 import { Input, knownInputs } from '../layout/Input'
 import { Section } from '../layout/Section'
@@ -115,11 +116,13 @@ const ModalBlocks = generateBlocksContainer({
     actions: generateActionsValidator(),
     context: true,
     divider: true,
+    header: true,
     image: true,
     input: true,
     section: generateSectionValidator(),
   },
   aliases: {
+    header: Header,
     hr: Divider,
     img: Image,
     input: Input,
@@ -141,6 +144,7 @@ const commonDefaultSubmit = plainText('Submit')
  * - `<Section>` (`<section>`)
  * - `<Image>` (`<img>`)
  * - `<Divider>` (`<hr>`)
+ * - `<Header>` (`<header>`)
  * - `<Context>`
  * - `<Actions>`
  * - `<Input>` (`<input>`)
diff --git a/src/block-kit/index.ts b/src/block-kit/index.ts
index debab0a..8e587e9 100644
--- a/src/block-kit/index.ts
+++ b/src/block-kit/index.ts
@@ -9,6 +9,7 @@ export { Call } from './layout/Call'
 export { Context } from './layout/Context'
 export { Divider } from './layout/Divider'
 export { File } from './layout/File'
+export { Header } from './layout/Header'
 export { Image } from './layout/Image'
 export { Input } from './layout/Input'
 export { Section, Field } from './layout/Section'
diff --git a/src/block-kit/layout/Header.ts b/src/block-kit/layout/Header.ts
new file mode 100644
index 0000000..df5121b
--- /dev/null
+++ b/src/block-kit/layout/Header.ts
@@ -0,0 +1,34 @@
+import { Block, PlainTextElement } from '@slack/types'
+import { JSXSlack, createComponent } from '../../jsx'
+import { plainText } from '../composition/utils'
+import { LayoutBlockProps } from './utils'
+
+export interface HeaderProps extends LayoutBlockProps {
+  children: JSXSlack.ChildElements
+}
+
+/**
+ * {@link https://api.slack.com/reference/messaging/blocks#header|The `header` layout block}
+ * to display plain text with bold font in a larger.
+ *
+ * ```jsx
+ * <Blocks>
+ *   <Header>
+ *     Heads up!
+ *   </Header>
+ * </Blocks>
+ * ```
+ *
+ * `<Header>` allows only a plain text. You cannot apply text styling through
+ * HTML-like tags.
+ *
+ * @return The partial JSON for `header` layout block
+ */
+export const Header = createComponent<
+  HeaderProps,
+  Block & { type: 'header'; text: PlainTextElement }
+>('Header', ({ blockId, children, id }) => ({
+  type: 'header',
+  block_id: blockId || id,
+  text: plainText(children, { layoutTags: true }),
+}))
diff --git a/src/jsx.ts b/src/jsx.ts
index b83fddb..60ff195 100644
--- a/src/jsx.ts
+++ b/src/jsx.ts
@@ -5,6 +5,7 @@ import { ButtonProps } from './block-kit/elements/Button'
 import { SelectProps } from './block-kit/elements/Select'
 import { TextareaProps } from './block-kit/input/Textarea'
 import { DividerProps } from './block-kit/layout/Divider'
+import { HeaderProps } from './block-kit/layout/Header'
 import { ImageProps } from './block-kit/layout/Image'
 import { InputProps } from './block-kit/layout/Input'
 import { SectionProps } from './block-kit/layout/Section'
@@ -410,6 +411,9 @@ export namespace JSXSlack {
   export namespace JSX {
     export interface Element extends Node {}
     export interface IntrinsicElements {
+      /** An HTML-compatible alias into `<Header>` layout block. */
+      header: HeaderProps
+
       /** An HTML-compatible alias into `<Divider>` layout block. */
       hr: DividerProps
 


Test patch:
diff --git a/test/block-kit/layout-blocks.tsx b/test/block-kit/layout-blocks.tsx
index afe5319..ad8b066 100644
--- a/test/block-kit/layout-blocks.tsx
+++ b/test/block-kit/layout-blocks.tsx
@@ -4,12 +4,14 @@ import {
   FileBlock,
   ImageBlock,
   InputBlock,
+  PlainTextElement,
   SectionBlock,
 } from '@slack/types'
 import {
   Actions,
   Blocks,
   Button,
+  Call,
   ChannelsSelect,
   Context,
   ConversationsSelect,
@@ -18,6 +20,8 @@ import {
   ExternalSelect,
   Field,
   File,
+  Header,
+  Home,
   Image,
   Input,
   JSXSlack,
@@ -28,7 +32,6 @@ import {
   Section,
   Select,
   UsersSelect,
-  Call,
 } from '../../src/index'
 
 beforeEach(() => JSXSlack.exactMode(false))
@@ -455,6 +458,77 @@ describe('Layout blocks', () => {
       ).toThrow())
   })
 
+  describe('<Header>', () => {
+    const text: PlainTextElement = {
+      type: 'plain_text',
+      text: 'Heads up!',
+      emoji: true,
+    }
+    const header = { type: 'header', block_id: 'header', text }
+
+    it('outputs header block', () => {
+      expect(<Header id="header">Heads up!</Header>).toStrictEqual(header)
+      expect(<Header blockId="header">Heads up!</Header>).toStrictEqual(header)
+    })
+
+    it('is aliased from <header> HTML intrinsic element by container', () => {
+      expect(
+        <Blocks>
+          <header id="header">Heads up!</header>
+        </Blocks>
+      ).toStrictEqual([header])
+
+      expect(
+        <Modal title="modal">
+          <header id="header">Heads up!</header>
+        </Modal>
+      ).toHaveProperty('blocks', [header])
+
+      expect(
+        <Home>
+          <header id="header">Heads up!</header>
+        </Home>
+      ).toHaveProperty('blocks', [header])
+    })
+
+    it('ignores HTML elements for styling', () => {
+      expect(
+        <Header>
+          <b>
+            Hello, <i>world!</i>
+          </b>
+        </Header>
+      ).toHaveProperty('text.text', 'Hello, world!')
+    })
+
+    it('allows using <br /> tag', () => {
+      expect(
+        <Header>
+          EXTRA!
+          <br />
+          EXTRA!
+          <br />
+          EXTRA!
+        </Header>
+      ).toHaveProperty('text.text', 'EXTRA!\nEXTRA!\nEXTRA!')
+    })
+
+    it.skip('allows using <p> tag [TODO]', () => {
+      expect(
+        <Header>
+          <p>Hello!</p>
+          <p>World!</p>
+        </Header>
+      ).toHaveProperty('text.text', 'Hello!\n\nWorld!')
+
+      expect(
+        <Header>
+          A<p>B</p>C
+        </Header>
+      ).toHaveProperty('text.text', 'A\n\nB\n\nC')
+    })
+  })
+
   describe('<File>', () => {
     const file: FileBlock = {
       block_id: 'file',