├── .nvmrc ├── codecov.yml ├── .github └── FUNDING.yml ├── jsx-runtime.mjs ├── src ├── prebundles │ ├── he.ts │ ├── mocks │ │ └── url.ts │ └── hastUtilToMdast.ts ├── components.ts ├── jsx-dev-runtime.ts ├── index.ts ├── block-kit │ ├── layout │ │ ├── Divider.ts │ │ ├── Call.ts │ │ ├── Header.ts │ │ ├── utils.ts │ │ ├── File.ts │ │ ├── Image.ts │ │ ├── Video.ts │ │ ├── Context.ts │ │ └── Actions.ts │ ├── elements │ │ ├── PlainTextInput.ts │ │ ├── UrlTextInput.ts │ │ ├── EmailTextInput.ts │ │ ├── NumberTextInput.ts │ │ ├── utils.ts │ │ ├── DatePicker.ts │ │ ├── TimePicker.ts │ │ ├── WorkflowButton.ts │ │ ├── Button.ts │ │ ├── DateTimePicker.ts │ │ ├── UsersSelect.ts │ │ ├── Overflow.ts │ │ ├── ChannelsSelect.ts │ │ ├── RadioButtonGroup.ts │ │ ├── CheckboxGroup.ts │ │ ├── ExternalSelect.ts │ │ ├── Select.tsx │ │ └── ConversationsSelect.ts │ ├── utils.ts │ ├── composition │ │ ├── Optgroup.ts │ │ ├── Option.ts │ │ ├── Checkbox.ts │ │ ├── RadioButton.ts │ │ ├── Confirm.ts │ │ └── utils.ts │ ├── index.ts │ ├── input │ │ └── Textarea.tsx │ ├── container │ │ ├── Blocks.ts │ │ └── utils.ts │ └── other │ │ └── SelectFragment.ts ├── jsx-runtime.ts ├── mrkdwn │ ├── parser.ts │ ├── measure.ts │ ├── escape.ts │ └── index.ts ├── error.ts ├── data │ └── font-width.json ├── utils.ts ├── tag.ts └── date.ts ├── jsx-dev-runtime.mjs ├── __mocks__ ├── jsx-runtime.js └── jsx-dev-runtime.js ├── docs ├── jsx.png ├── confirmation.png ├── slack-example.png ├── custom-header-block.png ├── highlight │ ├── v2-jsdoc.png │ ├── v2-jsx-error.png │ └── v2-repl-dark.png ├── slack-notification.png ├── conversations-select-current.gif ├── jsx-components-for-block-kit.md ├── preview-btn.svg └── about-escape-and-exact-mode.md ├── tools ├── .eslintrc.yml ├── version.js └── measure-font.js ├── .eslintignore ├── jsx-runtime.js ├── demo ├── public │ ├── favicon.ico │ ├── assets │ │ └── ogp.png │ ├── logo.svg │ ├── logo-type.svg │ └── loading.svg └── js │ ├── example.js │ ├── codemirror.css │ ├── convert.js │ ├── parse-hash.js │ ├── examples │ └── basic.js │ └── index.js ├── jsx-dev-runtime.js ├── jsx-runtime.d.ts ├── jsx-dev-runtime.d.ts ├── jest.config.esm.mjs ├── .prettierignore ├── test ├── babel │ ├── babel.config.js │ ├── production.jsx │ ├── classic.jsx │ └── automatic.jsx ├── .eslintrc.yml ├── esm.mjs └── tag.tsx ├── tsconfig.json ├── babel.config.js ├── LICENSE ├── jest.config.js ├── rollup.demo.config.mjs ├── .eslintrc.js ├── rollup.prebundle.config.mjs ├── rollup.config.mjs ├── .circleci └── config.yml ├── package.json └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.11.0 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [yhatt] 2 | -------------------------------------------------------------------------------- /jsx-runtime.mjs: -------------------------------------------------------------------------------- 1 | export * from './module/src/jsx-runtime.mjs' 2 | -------------------------------------------------------------------------------- /src/prebundles/he.ts: -------------------------------------------------------------------------------- 1 | import he from 'he' 2 | 3 | export { he } 4 | -------------------------------------------------------------------------------- /jsx-dev-runtime.mjs: -------------------------------------------------------------------------------- 1 | export * from './module/src/jsx-dev-runtime.mjs' 2 | -------------------------------------------------------------------------------- /__mocks__/jsx-runtime.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../src/jsx-runtime') 2 | -------------------------------------------------------------------------------- /docs/jsx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/docs/jsx.png -------------------------------------------------------------------------------- /src/prebundles/mocks/url.ts: -------------------------------------------------------------------------------- 1 | export function URL() { 2 | // empty 3 | } 4 | -------------------------------------------------------------------------------- /tools/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | '@typescript-eslint/no-var-requires': off 3 | -------------------------------------------------------------------------------- /__mocks__/jsx-dev-runtime.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../src/jsx-dev-runtime') 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | dist/* 3 | lib/* 4 | module/* 5 | types/* 6 | vendor/* 7 | -------------------------------------------------------------------------------- /docs/confirmation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/docs/confirmation.png -------------------------------------------------------------------------------- /jsx-runtime.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./lib/jsx-runtime.js') 4 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/demo/public/favicon.ico -------------------------------------------------------------------------------- /docs/slack-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/docs/slack-example.png -------------------------------------------------------------------------------- /jsx-dev-runtime.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./lib/jsx-dev-runtime.js') 4 | -------------------------------------------------------------------------------- /demo/public/assets/ogp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/demo/public/assets/ogp.png -------------------------------------------------------------------------------- /docs/custom-header-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/docs/custom-header-block.png -------------------------------------------------------------------------------- /docs/highlight/v2-jsdoc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/docs/highlight/v2-jsdoc.png -------------------------------------------------------------------------------- /docs/slack-notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/docs/slack-notification.png -------------------------------------------------------------------------------- /docs/highlight/v2-jsx-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/docs/highlight/v2-jsx-error.png -------------------------------------------------------------------------------- /docs/highlight/v2-repl-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/docs/highlight/v2-repl-dark.png -------------------------------------------------------------------------------- /jsx-runtime.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | export * from './types/src/jsx-runtime' 3 | -------------------------------------------------------------------------------- /jsx-dev-runtime.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | export * from './types/src/jsx-dev-runtime' 3 | -------------------------------------------------------------------------------- /docs/conversations-select-current.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/jsx-slack/HEAD/docs/conversations-select-current.gif -------------------------------------------------------------------------------- /jest.config.esm.mjs: -------------------------------------------------------------------------------- 1 | import defaultConfig from './jest.config.js' 2 | 3 | export default { 4 | ...defaultConfig, 5 | testMatch: ['/test/**/!(_)*.m[jt]s?(x)'], 6 | } 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .parcel-cache/ 3 | .git/ 4 | .vscode/ 5 | coverage/ 6 | dist/ 7 | lib/ 8 | module/ 9 | types/ 10 | vendor/ 11 | node_modules 12 | package.json 13 | -------------------------------------------------------------------------------- /test/babel/babel.config.js: -------------------------------------------------------------------------------- 1 | // This is workaround for https://github.com/facebook/jest/issues/12000. 2 | // Please remove if Jest was fixed to parse babel config placed to the root. 3 | module.exports = require('../../babel.config') 4 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - jest 3 | 4 | extends: 5 | - plugin:jest/recommended 6 | - plugin:jest/style 7 | 8 | rules: 9 | '@typescript-eslint/ban-ts-comment': 10 | - error 11 | - ts-expect-error: false 12 | -------------------------------------------------------------------------------- /src/components.ts: -------------------------------------------------------------------------------- 1 | import { JSXSlack } from './jsx' 2 | 3 | export * from './block-kit/index' 4 | export { Escape } from './mrkdwn/jsx' 5 | 6 | /** An alias into `JSXSlack.Fragment`, to group a list of JSX elements. */ 7 | export const Fragment = JSXSlack.Fragment 8 | -------------------------------------------------------------------------------- /test/babel/production.jsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource jsx-slack */ 2 | jest.mock('../../jsx-runtime') 3 | 4 | describe('Babel transpilation through automatic runtime (Production mode)', () => { 5 | it('does not have __source prop', () => { 6 | const Component = ({ __source }) => __source 7 | expect().toBeUndefined() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/prebundles/hastUtilToMdast.ts: -------------------------------------------------------------------------------- 1 | import { defaultHandlers, defaultNodeHandlers } from 'hast-util-to-mdast' 2 | 3 | export { toMdast as hastUtilToMdast } from 'hast-util-to-mdast' 4 | 5 | export const hastUtilToMdastListItem = defaultHandlers.li 6 | export const hastUtilToMdastTextarea = defaultHandlers.textarea 7 | export const hastUtilToMdastRoot = defaultNodeHandlers.root 8 | -------------------------------------------------------------------------------- /src/jsx-dev-runtime.ts: -------------------------------------------------------------------------------- 1 | import { jsx, Fragment, JSX } from './jsx-runtime' 2 | 3 | export const jsxDEV = ( 4 | type: any, 5 | props: Record, 6 | key: any, 7 | _: boolean, // isStaticChildren: not used in jsx-slack 8 | __source: Record, 9 | ) => jsx(type, { ...props, __source }, key) 10 | 11 | export { Fragment } 12 | export type { JSX } 13 | -------------------------------------------------------------------------------- /demo/js/example.js: -------------------------------------------------------------------------------- 1 | import * as appHomeExamples from './examples/app-home' 2 | import * as basicExamples from './examples/basic' 3 | import * as messagingExamples from './examples/messaging' 4 | import * as modalExamples from './examples/modal' 5 | 6 | export default Object.freeze( 7 | Object.assign( 8 | Object.create(null), 9 | basicExamples, 10 | messagingExamples, 11 | modalExamples, 12 | appHomeExamples, 13 | ), 14 | ) 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react", 5 | "lib": [ 6 | "es2019", 7 | "es2020.bigint", 8 | "es2020.string", 9 | "es2020.symbol.wellknown", 10 | "dom" 11 | ], 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "noImplicitAny": false, 15 | "resolveJsonModule": true, 16 | "sourceMap": true, 17 | "strict": true, 18 | "target": "es2019", 19 | "noErrorTruncation": true 20 | }, 21 | "include": ["src", "test"] 22 | } 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import { JSXSlack } from './jsx' 3 | 4 | export { JSXSlack } 5 | export * from './components' 6 | export { jsxslack } from './tag' 7 | export default JSXSlack 8 | 9 | // Useful type aliases that are similar to @types/react 10 | export type Node = JSXSlack.ChildElements 11 | export type FunctionComponent

= JSXSlack.FunctionComponent

12 | export type FC

= JSXSlack.FC

13 | export type PropsWithChildren

= JSXSlack.PropsWithChildren

14 | -------------------------------------------------------------------------------- /src/block-kit/layout/Divider.ts: -------------------------------------------------------------------------------- 1 | import { DividerBlock } from '@slack/types' 2 | import { createComponent } from '../../jsx-internals' 3 | import { LayoutBlockProps } from './utils' 4 | 5 | export interface DividerProps extends LayoutBlockProps { 6 | children?: never 7 | } 8 | 9 | /** 10 | * [The `divider` layout block](https://api.slack.com/reference/messaging/blocks#divider) 11 | * just to insert a divider. 12 | * 13 | * @return The partial JSON for `divider` layout block 14 | */ 15 | export const Divider = createComponent( 16 | 'Divider', 17 | ({ blockId, id }) => ({ type: 'divider', block_id: blockId || id }), 18 | ) 19 | -------------------------------------------------------------------------------- /tools/version.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | if (process.env.npm_package_version.match(/^\d+\.\d+\.\d+$/)) { 6 | const unreleased = '## [Unreleased]' 7 | const [date] = new Date().toISOString().split('T') 8 | const version = `## v${process.env.npm_package_version} - ${date}` 9 | 10 | const changelog = path.resolve(__dirname, '../CHANGELOG.md') 11 | const content = fs.readFileSync(changelog, 'utf8') 12 | 13 | fs.writeFileSync( 14 | changelog, 15 | content.replace(unreleased, `${unreleased}\n\n${version}`), 16 | ) 17 | } else { 18 | console.info("Detected not formal release version so CHANGELOG won't update.") 19 | } 20 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // NOTE: jsx-slack uses Babel only for testing. 2 | const babelPresets = (development) => [ 3 | ['@babel/preset-env', { targets: { node: true } }], 4 | ['@babel/preset-react', { development, runtime: 'automatic' }], 5 | ] 6 | 7 | module.exports = { 8 | presets: babelPresets(true), 9 | overrides: [ 10 | { 11 | test: './src/**/*', 12 | presets: ['@babel/preset-typescript', { allowNamespaces: true }], 13 | }, 14 | { 15 | test: './test/**/production.jsx', 16 | presets: babelPresets(false), 17 | }, 18 | ], 19 | plugins: [ 20 | [ 21 | 'transform-rename-import', 22 | { replacements: [{ original: '^node:(.+)$', replacement: '$1' }] }, 23 | ], 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /src/jsx-runtime.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-namespace */ 2 | import { JSXSlack } from './jsx' 3 | import { createElementInternal, FragmentInternal } from './jsx-internals' 4 | 5 | export const jsx = (type: any, props: Record, key: any) => 6 | createElementInternal(type ?? FragmentInternal, { 7 | ...props, 8 | ...(key !== undefined ? { key } : {}), 9 | }) 10 | 11 | export const jsxs = jsx 12 | export const Fragment = FragmentInternal 13 | 14 | export namespace JSX { 15 | export interface Element extends JSXSlack.JSX.Element {} 16 | export interface IntrinsicElements extends JSXSlack.JSX.IntrinsicElements {} 17 | export interface ElementChildrenAttribute 18 | extends JSXSlack.JSX.ElementChildrenAttribute {} 19 | } 20 | -------------------------------------------------------------------------------- /demo/js/codemirror.css: -------------------------------------------------------------------------------- 1 | @import url('codemirror/lib/codemirror.css'); 2 | @import url('codemirror/theme/tomorrow-night-bright.css'); 3 | @import url('codemirror/addon/hint/show-hint.css'); 4 | 5 | .CodeMirror-hints { 6 | background-color: var(--background); 7 | border-color: var(--border); 8 | box-shadow: 2px 3px 5px rgba(128, 128, 128, 0.15); 9 | 10 | scrollbar-color: rgba(128, 128, 128, 0.5) transparent; 11 | scrollbar-width: thin; 12 | } 13 | 14 | .CodeMirror-hints::-webkit-scrollbar { 15 | width: 10px; 16 | height: 10px; 17 | } 18 | 19 | .CodeMirror-hints::-webkit-scrollbar-thumb { 20 | background-color: rgba(128, 128, 128, 0.5); 21 | background-clip: padding-box; 22 | border: 2px solid transparent; 23 | border-radius: 8px; 24 | } 25 | 26 | .CodeMirror-hint { 27 | color: var(--primary-color); 28 | } 29 | -------------------------------------------------------------------------------- /src/block-kit/layout/Call.ts: -------------------------------------------------------------------------------- 1 | import { Block } from '@slack/types' 2 | import { createComponent } from '../../jsx-internals' 3 | import { LayoutBlockProps } from './utils' 4 | 5 | type CallBlock = Block & { 6 | type: 'call' 7 | call_id: string 8 | } 9 | 10 | export interface CallProps extends LayoutBlockProps { 11 | children?: never 12 | 13 | /** A string of registered call's ID. */ 14 | callId: string 15 | } 16 | 17 | /** 18 | * The `call` layout block to display your registered call to Slack. 19 | * 20 | * _This component is available only in `` container for messaging._ 21 | * 22 | * Learn about [using the Calls API](https://api.slack.com/apis/calls) in 23 | * the document of Slack API. 24 | * 25 | * @return The partial JSON for `call` layout block 26 | */ 27 | export const Call = createComponent('Call', (props) => ({ 28 | type: 'call', 29 | block_id: props.blockId || props.id, 30 | call_id: props.callId, 31 | })) 32 | -------------------------------------------------------------------------------- /demo/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/block-kit/layout/Header.ts: -------------------------------------------------------------------------------- 1 | import { HeaderBlock } from '@slack/types' 2 | import { JSXSlack } from '../../jsx' 3 | import { createComponent } from '../../jsx-internals' 4 | import { plainText } from '../composition/utils' 5 | import { LayoutBlockProps } from './utils' 6 | 7 | export interface HeaderProps extends LayoutBlockProps { 8 | children: JSXSlack.ChildElements 9 | } 10 | 11 | /** 12 | * [The `header` layout block](https://api.slack.com/reference/messaging/blocks#header) 13 | * to display plain text with bold font in a larger. 14 | * 15 | * ```jsx 16 | * 17 | *

18 | * Heads up! 19 | *
20 | * 21 | * ``` 22 | * 23 | * `
` allows only a plain text. You cannot apply text styling through 24 | * HTML-like tags. 25 | * 26 | * @return The partial JSON for `header` layout block 27 | */ 28 | export const Header = createComponent( 29 | 'Header', 30 | ({ blockId, children, id }) => ({ 31 | type: 'header', 32 | block_id: blockId || id, 33 | text: plainText(children, { layoutTags: true }), 34 | }), 35 | ) 36 | -------------------------------------------------------------------------------- /src/block-kit/layout/utils.ts: -------------------------------------------------------------------------------- 1 | import { JSXSlackError } from '../../error' 2 | import { isValidElementFromComponent } from '../../jsx-internals' 3 | import { resolveTagName } from '../utils' 4 | 5 | export interface LayoutBlockProps { 6 | /** A string of unique identifier for the layout block. */ 7 | blockId?: string 8 | 9 | /** A HTML-compatible alias into `blockId` prop. */ 10 | id?: string 11 | } 12 | 13 | export const generateInputValidator = 14 | (from: string) => 15 | (element: unknown): never => { 16 | const tag = resolveTagName(element) 17 | const isComponent = isValidElementFromComponent(element) 18 | 19 | throw new JSXSlackError( 20 | `<${from}> cannot include the ${(() => { 21 | if (tag) { 22 | if (isComponent && tag !== '') { 23 | const tagName = tag.slice(1, -1) 24 | return `input component. Please remove "label" prop from <${tagName} label="...">.` 25 | } 26 | return `element for "input" type: ${tag}` 27 | } 28 | return 'element for "input" type.' 29 | })()}`, 30 | element, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/mrkdwn/parser.ts: -------------------------------------------------------------------------------- 1 | import htm from 'htm/mini' 2 | import { decodeEntity } from './escape' 3 | 4 | // Preserve text's special spaces that would be rendered in HTML 5 | // (hast-util-to-mdast will over-collapse many spaces against HTML spec) 6 | const decodeForMdast = (text: string): string => 7 | text 8 | .replace(/&/g, '&') 9 | .replace(/(?![\t\n\r ])\s/g, (sp) => `&#${sp.codePointAt(0)};`) 10 | 11 | const html2hastLight = htm.bind((tagName, props, ...children) => { 12 | const hast = { 13 | tagName, 14 | type: 'element', 15 | properties: {}, 16 | children: [] as any[], 17 | } 18 | 19 | for (const k of props ? Object.keys(props) : []) 20 | hast.properties[k] = decodeEntity(props[k]) 21 | 22 | for (const child of children) { 23 | const v = decodeEntity(child) 24 | 25 | hast.children.push( 26 | typeof v === 'string' ? { value: decodeForMdast(v), type: 'text' } : v, 27 | ) 28 | } 29 | 30 | return hast 31 | }) 32 | 33 | export default function rehypeLightParser(html: string): any { 34 | const { children } = html2hastLight([`${html}`] as any) 35 | return { type: 'root', children } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019- Yuki Hattori (yukihattori1116@gmail.com). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/block-kit/layout/File.ts: -------------------------------------------------------------------------------- 1 | import { FileBlock } from '@slack/types' 2 | import { createComponent } from '../../jsx-internals' 3 | import { LayoutBlockProps } from './utils' 4 | 5 | export interface FileProps extends LayoutBlockProps { 6 | children?: never 7 | 8 | /** A string of external unique ID for the remote file to show. */ 9 | externalId: string 10 | 11 | /** 12 | * Override `source` field. 13 | * 14 | * At the moment, you should not take care of this because only the default 15 | * value `remote` is available in Slack. 16 | */ 17 | source?: string 18 | } 19 | 20 | /** 21 | * [The `file` layout block](https://api.slack.com/reference/messaging/blocks#file) 22 | * to display a remote file. 23 | * 24 | * _This component is available only in `` container for messaging._ 25 | * 26 | * Learn about [adding remote files](https://api.slack.com/messaging/files/remote) 27 | * in the document of Slack API. 28 | * 29 | * @return The partial JSON for `file` layout block 30 | */ 31 | export const File = createComponent('File', (props) => ({ 32 | type: 'file', 33 | block_id: props.blockId || props.id, 34 | external_id: props.externalId, 35 | source: props.source || 'remote', 36 | })) 37 | -------------------------------------------------------------------------------- /src/mrkdwn/measure.ts: -------------------------------------------------------------------------------- 1 | import { letters, spaces } from '../data/font-width.json' 2 | 3 | const flippedSpaces = Object.keys(spaces).reduce( 4 | (obj, key) => ({ ...obj, [spaces[key]]: key }), 5 | {} as Record, 6 | ) 7 | 8 | const spaceWidth = Object.values(spaces).sort((a, b) => b - a) 9 | 10 | const indentCache = new Map() 11 | const measureCache = new Map() 12 | 13 | export function makeIndent(width: number): string { 14 | let indent = indentCache.get(width) 15 | 16 | if (indent === undefined) { 17 | indent = '' 18 | let targetWidth = width 19 | 20 | spaceWidth.forEach((w) => { 21 | const num = Math.floor(targetWidth / w) 22 | if (num > 0) indent += flippedSpaces[w].repeat(num) 23 | 24 | targetWidth -= w * num 25 | }) 26 | 27 | indentCache.set(width, indent) 28 | } 29 | 30 | return indent 31 | } 32 | 33 | export function measureWidth(bulletStr: string) { 34 | let width = measureCache.get(bulletStr) 35 | 36 | if (width === undefined) { 37 | // 25.6 is almost same width with the regular whitespace 38 | width = [...bulletStr].reduce((total, l) => total + (letters[l] ?? 25.6), 0) 39 | measureCache.set(bulletStr, width) 40 | } 41 | 42 | return width 43 | } 44 | -------------------------------------------------------------------------------- /src/block-kit/elements/PlainTextInput.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PlainTextInput as SlackPlainTextInput, 3 | DispatchActionConfig, 4 | } from '@slack/types' 5 | import { createComponent } from '../../jsx-internals' 6 | import { plainText } from '../composition/utils' 7 | 8 | export interface PlainTextInputProps { 9 | children?: never 10 | actionId?: string 11 | initialValue?: string 12 | maxLength?: number 13 | minLength?: number 14 | multiline?: boolean 15 | placeholder?: string 16 | dispatchActionConfig?: DispatchActionConfig 17 | focusOnLoad?: boolean 18 | } 19 | 20 | // NOTE: is not public component 21 | export const PlainTextInput = createComponent< 22 | PlainTextInputProps, 23 | SlackPlainTextInput 24 | >('PlainTextInput', (props) => ({ 25 | type: 'plain_text_input', 26 | action_id: props.actionId, 27 | placeholder: 28 | // Placeholder for input HTML element should disable emoji conversion 29 | props.placeholder 30 | ? plainText(props.placeholder, { emoji: false }) 31 | : undefined, 32 | initial_value: props.initialValue, 33 | multiline: props.multiline, 34 | max_length: props.maxLength, 35 | min_length: props.minLength, 36 | dispatch_action_config: props.dispatchActionConfig, 37 | focus_on_load: props.focusOnLoad, 38 | })) 39 | -------------------------------------------------------------------------------- /src/block-kit/elements/UrlTextInput.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PlainTextInput as SlackPlainTextInput, 3 | DispatchActionConfig, 4 | } from '@slack/types' 5 | import { createComponent } from '../../jsx-internals' 6 | import { plainText } from '../composition/utils' 7 | 8 | // TODO: Use official type when it was available in `@slack/types` 9 | interface SlackUrlTextInput 10 | extends Omit< 11 | SlackPlainTextInput, 12 | 'max_length' | 'min_length' | 'multiline' | 'type' 13 | > { 14 | type: 'url_text_input' 15 | } 16 | 17 | export interface UrlTextInputProps { 18 | children?: never 19 | actionId?: string 20 | initialValue?: string 21 | placeholder?: string 22 | dispatchActionConfig?: DispatchActionConfig 23 | focusOnLoad?: boolean 24 | } 25 | 26 | // NOTE: is not public component 27 | export const UrlTextInput = createComponent< 28 | UrlTextInputProps, 29 | SlackUrlTextInput 30 | >('UrlTextInput', (props) => ({ 31 | type: 'url_text_input', 32 | action_id: props.actionId, 33 | placeholder: 34 | // Placeholder for input HTML element should disable emoji conversion 35 | props.placeholder 36 | ? plainText(props.placeholder, { emoji: false }) 37 | : undefined, 38 | initial_value: props.initialValue, 39 | dispatch_action_config: props.dispatchActionConfig, 40 | focus_on_load: props.focusOnLoad, 41 | })) 42 | -------------------------------------------------------------------------------- /src/block-kit/elements/EmailTextInput.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PlainTextInput as SlackPlainTextInput, 3 | DispatchActionConfig, 4 | } from '@slack/types' 5 | import { createComponent } from '../../jsx-internals' 6 | import { plainText } from '../composition/utils' 7 | 8 | // TODO: Use official type when it was available in `@slack/types` 9 | interface SlackEmailTextInput 10 | extends Omit< 11 | SlackPlainTextInput, 12 | 'max_length' | 'min_length' | 'multiline' | 'type' 13 | > { 14 | type: 'email_text_input' 15 | } 16 | 17 | export interface EmailTextInputProps { 18 | children?: never 19 | actionId?: string 20 | initialValue?: string 21 | placeholder?: string 22 | dispatchActionConfig?: DispatchActionConfig 23 | focusOnLoad?: boolean 24 | } 25 | 26 | // NOTE: is not public component 27 | export const EmailTextInput = createComponent< 28 | EmailTextInputProps, 29 | SlackEmailTextInput 30 | >('EmailTextInput', (props) => ({ 31 | type: 'email_text_input', 32 | action_id: props.actionId, 33 | placeholder: 34 | // Placeholder for input HTML element should disable emoji conversion 35 | props.placeholder 36 | ? plainText(props.placeholder, { emoji: false }) 37 | : undefined, 38 | initial_value: props.initialValue, 39 | dispatch_action_config: props.dispatchActionConfig, 40 | focus_on_load: props.focusOnLoad, 41 | })) 42 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const esModules = [ 2 | 'hast-util-embedded', 3 | 'hast-util-has-property', 4 | 'hast-util-is-body-ok-link', 5 | 'hast-util-is-element', 6 | 'hast-util-phrasing', 7 | 'hast-util-to-mdast', 8 | 'hast-util-to-text', 9 | 'hast-util-whitespace', 10 | 'mdast-util-phrasing', 11 | 'mdast-util-to-string', 12 | 'rehype-minify-whitespace', 13 | 'trim-trailing-lines', 14 | 'unist-util-find-after', 15 | 'unist-util-is', 16 | 'unist-util-parents', 17 | 'unist-util-position', 18 | 'unist-util-visit', 19 | ] 20 | 21 | module.exports = { 22 | collectCoverageFrom: [ 23 | 'src/**/*.js', 24 | 'src/**/*.jsx', 25 | 'src/**/*.ts', 26 | 'src/**/*.tsx', 27 | ], 28 | coveragePathIgnorePatterns: [ 29 | '/node_modules/', 30 | '.*\\.d\\.ts', 31 | 'prebundles/mocks', 32 | ], 33 | coverageThreshold: { global: { lines: 95 } }, 34 | moduleFileExtensions: [ 35 | 'js', 36 | 'jsx', 37 | 'cjs', 38 | 'mjs', 39 | 'ts', 40 | 'tsx', 41 | 'json', 42 | 'node', 43 | ], 44 | moduleNameMapper: { 45 | '^jsx-slack(.*)$': '$1', 46 | }, 47 | preset: 'ts-jest/presets/js-with-babel', 48 | resetMocks: true, 49 | restoreMocks: true, 50 | testEnvironment: 'node', 51 | testMatch: ['/test/**/!(_)*.[jt]s?(x)'], 52 | testPathIgnorePatterns: ['/node_modules/', 'babel.config.js'], 53 | transformIgnorePatterns: [`/node_modules/(?!${esModules.join('|')})`], 54 | } 55 | -------------------------------------------------------------------------------- /rollup.demo.config.mjs: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import json from '@rollup/plugin-json' 4 | import nodeResolve from '@rollup/plugin-node-resolve' 5 | import cssnano from 'cssnano' 6 | import postcssImport from 'postcss-import' 7 | import copy from 'rollup-plugin-copy' 8 | import esbuild from 'rollup-plugin-esbuild' 9 | import livereload from 'rollup-plugin-livereload' 10 | import postcss from 'rollup-plugin-postcss' 11 | import serve from 'rollup-plugin-serve' 12 | import { prebundleAlias, prebundleConfig } from './rollup.prebundle.config.mjs' 13 | 14 | const require = createRequire(import.meta.url) 15 | const { compilerOptions } = require('./tsconfig.json') 16 | 17 | export default [ 18 | prebundleConfig, 19 | { 20 | plugins: [ 21 | prebundleAlias, 22 | json({ preferConst: true }), 23 | esbuild({ 24 | minify: !process.env.ROLLUP_WATCH, 25 | target: compilerOptions.target, 26 | }), 27 | nodeResolve(), 28 | commonjs(), 29 | postcss({ 30 | plugins: [postcssImport, cssnano({ preset: 'default' })], 31 | }), 32 | copy({ targets: [{ src: 'demo/public/**/*', dest: 'dist' }] }), 33 | process.env.ROLLUP_WATCH && serve({ contentBase: 'dist' }), 34 | process.env.ROLLUP_WATCH && livereload(), 35 | ], 36 | input: ['demo/js/index.js'], 37 | output: { dir: 'dist', format: 'iife', compact: true }, 38 | }, 39 | ] 40 | -------------------------------------------------------------------------------- /src/block-kit/utils.ts: -------------------------------------------------------------------------------- 1 | import { JSXSlack } from '../jsx' 2 | import { 3 | cleanMeta, 4 | createElementInternal, 5 | isValidComponent, 6 | } from '../jsx-internals' 7 | 8 | export const assignMetaFrom = ( 9 | element: JSXSlack.Node, 10 | obj: T, 11 | ): T & JSXSlack.Node => 12 | Object.defineProperty(obj as any, '$$jsxslack', { value: element.$$jsxslack }) 13 | 14 | export const alias = ( 15 | element: JSXSlack.Node, 16 | to: JSXSlack.FC, 17 | 18 | /** 19 | * Whether preserve metadata from the origin of alias. A resolved tag name 20 | * would not make surprise to user. 21 | * 22 | * **WARN**: Turn off this by setting `false` if the aliased element had 23 | * non-enumerable unique metadata. 24 | */ 25 | preserveOriginMeta = true, 26 | ): JSXSlack.Node | null => { 27 | const aliased = createElementInternal( 28 | to, 29 | element.$$jsxslack.props, 30 | ...element.$$jsxslack.children, 31 | ) 32 | 33 | return preserveOriginMeta && typeof aliased === 'object' && aliased 34 | ? assignMetaFrom(element, cleanMeta(aliased)) 35 | : aliased 36 | } 37 | 38 | export const resolveTagName = (element: unknown): string | undefined => { 39 | if (JSXSlack.isValidElement(element)) { 40 | if (typeof element.$$jsxslack.type === 'string') 41 | return `<${element.$$jsxslack.type}>` 42 | 43 | if (isValidComponent(element.$$jsxslack.type)) 44 | return `<${element.$$jsxslack.type.$$jsxslackComponent.name}>` 45 | } 46 | return undefined 47 | } 48 | -------------------------------------------------------------------------------- /demo/js/convert.js: -------------------------------------------------------------------------------- 1 | import { JSXSlack, jsxslack } from '../../src/index' 2 | import { isValidComponent } from '../../src/jsx-internals' 3 | 4 | const generateUrl = (json) => 5 | `https://api.slack.com/tools/block-kit-builder#${encodeURIComponent( 6 | JSON.stringify(json), 7 | )}` 8 | 9 | export const convert = (jsx) => { 10 | const output = jsxslack([jsx]) 11 | 12 | if (!JSXSlack.isValidElement(output)) 13 | throw new Error('Cannot parse as jsx-slack component.') 14 | 15 | const ret = { text: JSON.stringify(output, null, 2) } 16 | 17 | if (isValidComponent(output.$$jsxslack.type)) { 18 | const container = output.$$jsxslack 19 | const { name: containerName } = container.type.$$jsxslackComponent 20 | 21 | const checkVideoBlock = () => { 22 | const children = JSXSlack.Children.toArray(container.children) 23 | if ( 24 | children.find( 25 | (block) => 26 | block.type === 'video' || block.$$jsxslack.type === 'video', 27 | ) 28 | ) { 29 | ret.tooltip = 30 | 'NOTE: The video layout block may not test and preview in Slack Block Kit Builder.' 31 | } 32 | } 33 | 34 | if (containerName === 'Blocks') { 35 | ret.url = generateUrl({ blocks: output }) 36 | checkVideoBlock() 37 | } else if (containerName === 'Modal' || containerName === 'Home') { 38 | if (output.type === 'modal' || output.type === 'home') { 39 | ret.url = generateUrl(output) 40 | checkVideoBlock() 41 | } 42 | } 43 | } 44 | 45 | return ret 46 | } 47 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').ESLint.ConfigData} */ 2 | module.exports = { 3 | root: true, 4 | env: { 5 | browser: true, 6 | es6: true, 7 | node: true, 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:import/recommended', 12 | 'plugin:import/react', 13 | 'plugin:react/recommended', 14 | 'prettier', 15 | ], 16 | reportUnusedDisableDirectives: true, 17 | rules: { 18 | 'import/namespace': ['error', { allowComputed: true }], 19 | 'import/order': ['error', { alphabetize: { order: 'asc' } }], 20 | 'no-console': 'warn', 21 | 'react/jsx-key': 'off', 22 | 'react/no-children-prop': 'off', 23 | 'react/prop-types': 'off', 24 | 'react/no-unknown-property': 'off', 25 | }, 26 | settings: { 27 | 'import/resolver': { 28 | node: { extensions: ['.mjs', '.js', '.jsx', '.json', '.ts', '.tsx'] }, 29 | }, 30 | 'import/ignore': ['@rollup/plugin-node-resolve'], 31 | react: { pragma: 'JSXSlack' }, 32 | }, 33 | overrides: [ 34 | { 35 | files: ['**/*.ts', '**/*.tsx'], 36 | parser: '@typescript-eslint/parser', 37 | plugins: ['@typescript-eslint'], 38 | extends: [ 39 | 'plugin:@typescript-eslint/recommended', 40 | 'plugin:import/typescript', 41 | 'prettier', 42 | ], 43 | rules: { 44 | '@typescript-eslint/no-explicit-any': 'off', 45 | '@typescript-eslint/explicit-function-return-type': 'off', 46 | '@typescript-eslint/explicit-module-boundary-types': 'off', 47 | }, 48 | }, 49 | ], 50 | } 51 | -------------------------------------------------------------------------------- /tools/measure-font.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const fs = require('fs') 3 | const path = require('path') 4 | const puppeteer = require('puppeteer') 5 | 6 | const saveTo = path.resolve(__dirname, '../src/data/font-width.json') 7 | 8 | // Measure width of latin letters and spaces in Slack Lato font 9 | ;(async () => { 10 | const browser = await puppeteer.launch() 11 | const page = await browser.newPage() 12 | 13 | await page.goto('https://api.slack.com/', { waitUntil: 'networkidle2' }) 14 | 15 | const measuredData = await page.evaluate(() => { 16 | // The bullet of list item may take these characters 17 | const letters = 18 | '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-.•◦▪' 19 | 20 | // Use Unicode spaces that would not be collapsed implicitly 21 | const spaces = '\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a' 22 | 23 | const measured = { letters: {}, spaces: {} } 24 | const canvas = document.createElement('canvas') 25 | const ctx = canvas.getContext('2d') 26 | 27 | ctx.font = '100px Slack-Lato, sans-serif' 28 | 29 | for (const l of letters) measured.letters[l] = ctx.measureText(l).width 30 | for (const s of spaces) measured.spaces[s] = ctx.measureText(s).width 31 | 32 | return measured 33 | }) 34 | 35 | await browser.close() 36 | 37 | return measuredData 38 | })().then((data) => { 39 | fs.mkdirSync(path.dirname(saveTo), { recursive: true }) 40 | fs.writeFileSync(saveTo, JSON.stringify(data, null, 2)) 41 | 42 | console.log(`Measured data of font width was saved to ${saveTo}.`) 43 | }) 44 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | import { resolveTagName } from './block-kit/utils' 2 | import { JSXSlack } from './jsx' 3 | 4 | interface JSXSource { 5 | columnNumber: number 6 | fileName: string 7 | lineNumber: number 8 | } 9 | 10 | const getSource = (source: unknown): JSXSource | undefined => { 11 | const src = JSXSlack.isValidElement(source) 12 | ? source.$$jsxslack.props?.__source 13 | : source 14 | 15 | if ( 16 | typeof src === 'object' && 17 | src && 18 | Object.prototype.hasOwnProperty.call(src, 'columnNumber') && 19 | Object.prototype.hasOwnProperty.call(src, 'fileName') && 20 | Object.prototype.hasOwnProperty.call(src, 'lineNumber') 21 | ) 22 | return src 23 | 24 | return undefined 25 | } 26 | 27 | export class JSXSlackError extends Error { 28 | /** An original stack trace. */ 29 | readonly originalStack?: string 30 | 31 | /** 32 | * Create JSXSlackError instance. 33 | * 34 | * @param message Error message 35 | * @param source JSX or `__source` property of the cause element 36 | */ 37 | constructor(message: string, source?: unknown) { 38 | super(message) 39 | 40 | this.name = new.target.name 41 | this.originalStack = this.stack 42 | 43 | Object.setPrototypeOf(this, new.target.prototype) 44 | this.resetStack(source) 45 | } 46 | 47 | private resetStack(source: unknown) { 48 | const src = getSource(source) 49 | if (!src) return 50 | 51 | const tag = resolveTagName(source) || 'JSX element' 52 | 53 | this.stack = `${this.name}: ${this.message} 54 | at ${tag} (${src.fileName}:${src.lineNumber}:${src.columnNumber})` 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rollup.prebundle.config.mjs: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | import path from 'node:path' 3 | import url from 'node:url' 4 | import alias from '@rollup/plugin-alias' 5 | import json from '@rollup/plugin-json' 6 | import esbuild from 'rollup-plugin-esbuild' 7 | 8 | const require = createRequire(import.meta.url) 9 | const { compilerOptions } = require('./tsconfig.json') 10 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) 11 | 12 | export const prebundleAlias = alias({ 13 | entries: [ 14 | { 15 | find: /^.*\bprebundles\/(.+)$/, 16 | replacement: path.resolve(__dirname, './vendor/$1.ts.mjs'), 17 | }, 18 | // Node's URL dependency will never use in jsx-slack 19 | { 20 | find: 'node:url', 21 | replacement: path.resolve(__dirname, './src/prebundles/mocks/url.ts'), 22 | }, 23 | ], 24 | }) 25 | 26 | const preBundles = ['src/prebundles/hastUtilToMdast.ts', 'src/prebundles/he.ts'] 27 | 28 | export const prebundleConfig = { 29 | plugins: [ 30 | json({ preferConst: true }), 31 | esbuild({ 32 | minify: !process.env.ROLLUP_WATCH, 33 | target: compilerOptions.target, 34 | optimizeDeps: { 35 | include: preBundles, 36 | exclude: ['node:url'], 37 | esbuildOptions: { outbase: __dirname, entryNames: '[dir]/[name].ts' }, 38 | }, 39 | }), 40 | ], 41 | input: preBundles, 42 | external: ['node:url'], 43 | output: { 44 | chunkFileNames: '[name]-[hash].mjs', 45 | dir: 'vendor', 46 | entryFileNames: '[name].mjs', 47 | exports: 'named', 48 | format: 'es', 49 | compact: true, 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /demo/public/logo-type.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/block-kit/elements/NumberTextInput.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PlainTextInput as SlackPlainTextInput, 3 | DispatchActionConfig, 4 | } from '@slack/types' 5 | import { createComponent } from '../../jsx-internals' 6 | import { plainText } from '../composition/utils' 7 | 8 | // TODO: Use official type when it was available in `@slack/types` 9 | interface SlackNumberTextInput 10 | extends Omit< 11 | SlackPlainTextInput, 12 | 'max_length' | 'min_length' | 'multiline' | 'type' 13 | > { 14 | type: 'number_input' 15 | is_decimal_allowed: boolean 16 | max_value?: string 17 | min_value?: string 18 | } 19 | 20 | export interface NumberTextInputProps { 21 | children?: never 22 | actionId?: string 23 | initialValue?: string 24 | placeholder?: string 25 | isDecimalAllowed?: boolean 26 | maxValue?: string 27 | minValue?: string 28 | dispatchActionConfig?: DispatchActionConfig 29 | focusOnLoad?: boolean 30 | } 31 | 32 | // NOTE: is not public component 33 | export const NumberTextInput = createComponent< 34 | NumberTextInputProps, 35 | SlackNumberTextInput 36 | >('NumberTextInput', (props) => ({ 37 | type: 'number_input', 38 | action_id: props.actionId, 39 | placeholder: 40 | // Placeholder for input HTML element should disable emoji conversion 41 | props.placeholder 42 | ? plainText(props.placeholder, { emoji: false }) 43 | : undefined, 44 | is_decimal_allowed: props.isDecimalAllowed ?? false, 45 | min_value: props.minValue, 46 | max_value: props.maxValue, 47 | initial_value: props.initialValue, 48 | dispatch_action_config: props.dispatchActionConfig, 49 | focus_on_load: props.focusOnLoad, 50 | })) 51 | -------------------------------------------------------------------------------- /demo/public/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/esm.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import { jsxslack, Fragment } from 'jsx-slack' 3 | import * as jsxDevRuntime from 'jsx-slack/jsx-dev-runtime' 4 | import * as jsxRuntime from 'jsx-slack/jsx-runtime' 5 | 6 | describe('ES modules', () => { 7 | describe('jsxslack template literal tag', () => { 8 | it('renders JSON object without using JSX transpiler', () => { 9 | expect(jsxslack` 10 | 11 |
12 | ES modules 13 |
14 |
15 | `).toMatchInlineSnapshot(` 16 | [ 17 | { 18 | "text": { 19 | "text": "ES modules", 20 | "type": "mrkdwn", 21 | "verbatim": true, 22 | }, 23 | "type": "section", 24 | }, 25 | ] 26 | `) 27 | }) 28 | }) 29 | 30 | describe('JSX runtime', () => { 31 | it('has jsx function and jsxs function', () => { 32 | expect(jsxRuntime).toHaveProperty('jsx', expect.any(Function)) 33 | expect(jsxRuntime).toHaveProperty('jsxs', expect.any(Function)) 34 | }) 35 | 36 | it('has exported Fragment', () => { 37 | expect(jsxRuntime).toHaveProperty('Fragment') 38 | expect(jsxRuntime.Fragment({ children: ['A', 'B'] })).toStrictEqual( 39 | Fragment({ children: ['A', 'B'] }), 40 | ) 41 | }) 42 | }) 43 | 44 | describe('JSX dev runtime', () => { 45 | it('has jsxDEV function', () => { 46 | expect(jsxDevRuntime).toHaveProperty('jsxDEV', expect.any(Function)) 47 | }) 48 | 49 | it('has exported Fragment', () => { 50 | expect(jsxDevRuntime).toHaveProperty('Fragment') 51 | expect(jsxDevRuntime.Fragment({ children: ['A', 'B'] })).toStrictEqual( 52 | Fragment({ children: ['A', 'B'] }), 53 | ) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/block-kit/composition/Optgroup.ts: -------------------------------------------------------------------------------- 1 | import { PlainTextElement } from '@slack/types' 2 | import { JSXSlackError } from '../../error' 3 | import { JSXSlack } from '../../jsx' 4 | import { createComponent } from '../../jsx-internals' 5 | import { alias, resolveTagName } from '../utils' 6 | import { Option, OptionComposition } from './Option' 7 | import { plainText } from './utils' 8 | 9 | export interface OptgroupComposition { 10 | label: PlainTextElement 11 | options: OptionComposition[] 12 | } 13 | 14 | export interface OptgroupProps { 15 | children: JSXSlack.ChildNodes 16 | 17 | /** A plain-text string for the label of the option group. */ 18 | label: string 19 | } 20 | 21 | /** 22 | * Generate the composition object, for the group of option items in the static 23 | * select element. 24 | * 25 | * It must contain `