Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions docs/layout-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,91 @@ Display a card of the call that was registered to Slack workspace. [Learn about
- `callId` (**required**): A string of registered call's ID.
- `id` / `blockId` (optional): A string of unique identifier of block.

## <a name="user-content-table" id="table"></a> [`<Table>`: Table Block](https://api.slack.com/reference/block-kit/blocks#table)

Display a table with rows and cells. This block allows you to present tabular data in a structured format.

```jsx
<Blocks>
<Table>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Age</TableCell>
<TableCell>Department</TableCell>
</TableRow>
<TableRow>
<TableCell>Alice</TableCell>
<TableCell>30</TableCell>
<TableCell>Engineering</TableCell>
</TableRow>
<TableRow>
<TableCell>Bob</TableCell>
<TableCell>25</TableCell>
<TableCell>Design</TableCell>
</TableRow>
</Table>
</Blocks>
```

### Props

- `id` / `blockId` (optional): A string of unique identifier of block.
- `columnSettings` (optional): An array of column settings to control alignment and wrapping.

### Column Settings

You can customize column appearance by providing `columnSettings` prop:

```jsx
<Blocks>
<Table
columnSettings={[
{ align: 'left', wrap: true },
{ align: 'right' },
{ align: 'center', wrap: false },
]}
>
<TableRow>
<TableCell>Product</TableCell>
<TableCell>Price</TableCell>
<TableCell>Status</TableCell>
</TableRow>
<TableRow>
<TableCell>Widget A</TableCell>
<TableCell>$99.99</TableCell>
<TableCell>Available</TableCell>
</TableRow>
</Table>
</Blocks>
```

Each column setting object can have:

- `align` (optional): Text alignment - `'left'`, `'center'`, or `'right'`. Default is `'left'`.
- `wrap` (optional): Whether text should wrap. Default is `false`.

### <a name="user-content-table-row" id="table-row"></a> `<TableRow>`: Table row

Represents a single row in the table. Each row contains `<TableCell>` components.

### <a name="user-content-table-cell" id="table-cell"></a> `<TableCell>`: Table cell

Represents a single cell within a table row. The content will be converted to plain text.

```jsx
<TableRow>
<TableCell>Text content</TableCell>
<TableCell>{123}</TableCell>
</TableRow>
```

### Constraints

- Maximum 100 rows per table
- Maximum 20 cells per row
- Maximum 20 column settings
- One table per message (Slack limitation)

---

###### [Top](../README.md) &raquo; [JSX components for Block Kit](jsx-components-for-block-kit.md) &raquo; Layout blocks
2 changes: 2 additions & 0 deletions src/block-kit/container/Blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
* - `<Video>` (`<video>`)
* - `<File>`
* - `<Call>`
* - `<Table>`
*
* And these input components (Require defining `label` prop):
*
Expand Down Expand Up @@ -77,6 +78,7 @@ export const Blocks: BuiltInComponent<BlocksProps> = generateBlocksContainer({
image: true,
input: true,
section: generateSectionValidator(availableSectionAccessoryTypes),
table: true,
video: true,
},
aliases: {
Expand Down
1 change: 1 addition & 0 deletions src/block-kit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { Header } from './layout/Header'
export { Image } from './layout/Image'
export { Input } from './layout/Input'
export { Section, Field } from './layout/Section'
export { Table, TableRow, TableCell } from './layout/Table'
export { Video } from './layout/Video'

// Block elements & interactive elements
Expand Down
141 changes: 141 additions & 0 deletions src/block-kit/layout/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Block } from '@slack/types'
import { JSXSlackError } from '../../error'
import { JSXSlack } from '../../jsx'
import { createComponent } from '../../jsx-internals'
import { LayoutBlockProps } from './utils'

interface TableBlock extends Block {
type: 'table'
rows: TableCellObject[][]
column_settings?: ColumnSetting[]
}

interface TableCellObject {
type: 'raw_text' | 'rich_text'
text: string
}

interface ColumnSetting {
align?: 'left' | 'center' | 'right'
is_wrapped?: boolean
}

interface ColumnSettingProp {
align?: 'left' | 'center' | 'right'
wrap?: boolean
}

const tableRowSymbol = Symbol('jsx-slack-table-row')
const tableCellSymbol = Symbol('jsx-slack-table-cell')

export interface TableProps extends LayoutBlockProps {
children?: JSXSlack.ChildElements
columnSettings?: ColumnSettingProp[]
}

interface TableRowProps {
children: JSXSlack.ChildElements
}

interface TableCellProps {
children: JSXSlack.ChildElements
}

export const TableCell = createComponent<TableCellProps, TableCellObject>(
'TableCell',
({ children }) => {
const text = JSXSlack.Children.toArray(children)
.map((child) => {
if (typeof child === 'string') return child
if (typeof child === 'number') return String(child)
return ''
})
.join('')

return Object.defineProperty(
{ type: 'raw_text' as const, text },
tableCellSymbol,
{ value: true },
)
},
)

export const TableRow = createComponent<TableRowProps, TableCellObject[]>(
'TableRow',
({ children }) => {
const cells: TableCellObject[] = []

JSXSlack.Children.toArray(children).forEach((child) => {
if (
typeof child === 'object' &&
child !== null &&
tableCellSymbol in child
) {
cells.push(child as unknown as TableCellObject)
}
})

if (cells.length > 20) {
throw new JSXSlackError(
`<TableRow> can contain up to 20 cells, but there are ${cells.length} cells.`,
children,
)
}

return Object.defineProperty(cells, tableRowSymbol, { value: true })
},
)

export const Table = createComponent<TableProps, TableBlock>(
'Table',
({ blockId, children, id, columnSettings, ...rest }) => {
const rows: TableCellObject[][] = []

if (children) {
JSXSlack.Children.toArray(children).forEach((child) => {
if (
Array.isArray(child) &&
tableRowSymbol in child
) {
rows.push(child as TableCellObject[])
}
})
}

if (rows.length > 100) {
throw new JSXSlackError(
`<Table> can contain up to 100 rows, but there are ${rows.length} rows.`,
rest['__source'],
)
}

let column_settings: ColumnSetting[] | undefined

if (columnSettings) {
if (columnSettings.length > 20) {
throw new JSXSlackError(
`<Table> columnSettings can contain up to 20 items, but there are ${columnSettings.length} items.`,
rest['__source'],
)
}

column_settings = columnSettings.map((setting) => {
const result: ColumnSetting = {}
if (setting.align !== undefined) {
result.align = setting.align
}
if (setting.wrap !== undefined) {
result.is_wrapped = setting.wrap
}
return result
})
}

return {
type: 'table',
block_id: blockId || id,
rows,
column_settings,
}
},
)
Loading