Skip to content

Commit 89b4473

Browse files
authored
[PLAY-3207] Table Kit: Add ColSpan Support + Docs to React and Rails (#6596)
**What does this PR do?** A clear and concise description with your runway ticket url. [PLAY-3207](https://runway.powerhrg.com/backlog_items/PLAY-3207) adds the colSpan prop to the React Table Header and Table Cell, colspan to the Rails Table Cell, and adds docs for the prop. Rails Table Header already has colspan, and this is a precursor story to creating a Custom Sort embedded in the Header React doc example. **Screenshots:** Screenshots to visualize your addition/change <img width="1127" height="566" alt="doc with vertical borders" src="https://github.com/user-attachments/assets/cf0e7726-2f3c-4a5f-a276-ce8925299508" /> **How to test?** Steps to confirm the desired behavior: 1. Go to the Table with Colspan doc example ([rails](linktocome), [react](linktocome)) to see colspan in action. Playground test: no prop override but can edit children to give Table.Header or Table.Cell a colSpan example code: ``` <Table.Head> <Table.Row> <Table.Header colSpan={2} text="Territory ID" /> <Table.Header text="Name" /> <Table.Header text="Status" /> </Table.Row> </Table.Head> <Table.Body> <Table.Row> <Table.Cell colSpan={2} text="1001" numberSpacing="tabular" /> <Table.Cell text="Ada Lovelace" /> <Table.Cell text="Active" /> </Table.Row> <Table.Row> <Table.Cell text="1002" numberSpacing="tabular" /> <Table.Cell text="Grace Hopper" /> <Table.Cell text="Pending" /> <Table.Cell text="Apr 10, 2026" textAlign="right" /> </Table.Row> <Table.Row> <Table.Cell text="1003" numberSpacing="tabular" /> <Table.Cell text="Katherine Johnson" /> <Table.Cell text="Active" /> <Table.Cell text="Apr 11, 2026" textAlign="right" /> </Table.Row> </Table.Body> ``` #### Checklist: - [x] **LABELS** Add a label: `enhancement`, `bug`, `improvement`, `new kit`, `deprecated`, or `breaking`. See [Changelog & Labels](https://github.com/powerhome/playbook/wiki/Changelog-&-Labels) for details. - [x] **DEPLOY** I have added the `milano` label to show I'm ready for a review. - [x] **TESTS** I have added test coverage to my code. - [x] **PLAYGROUND** I have added and tested Playground metadata and overrides for all kits and props updated in my code. - [x] **SEMVER** I have added a `minor`, `major`, or `patch` label for release. - [ ] **RC** I have added an `inactive RC` label if not an active RC.
1 parent 0546125 commit 89b4473

16 files changed

Lines changed: 239 additions & 7 deletions

playbook/app/pb_kits/playbook/pb_table/_table.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ type TableProps = {
4949
verticalBorder?: boolean,
5050
} & GlobalProps
5151

52+
/**
53+
* Subcomponent props (Table.Header / Table.Cell). Must be a plain `type NameProps = { ... }` block—
54+
* `yarn generate-ai-metadata` only parses that shape in this file (not Omit<> / intersection).
55+
* Not used on the root component; playground routes these via propTargets.
56+
*/
57+
type _TableSubkitSchemaProps = {
58+
colSpan?: number
59+
}
60+
5261
type AllSizes = "none" | "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl" | "auto" | "initial" | "inherit"
5362

5463
const Table = (props: TableProps): React.ReactElement => {

playbook/app/pb_kits/playbook/pb_table/docs/_playground.json

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
"template": "<Table{{props}} />",
3-
"propTargets": {},
3+
"propTargets": {
4+
"colSpan": "Table.Header.props"
5+
},
46
"defaults": {
57
"collapse": "sm",
68
"container": true,
@@ -50,6 +52,12 @@
5052
"stickyRightColumn"
5153
]
5254
},
55+
{
56+
"name": "Subcomponent Props",
57+
"props": [
58+
"colSpan"
59+
]
60+
},
5361
{
5462
"name": "Filter Variant",
5563
"props": [
@@ -224,6 +232,13 @@
224232
"presetName": "With Filter",
225233
"message": "With Filter shows the withFilter wrapper while keeping all table children editable.",
226234
"type": "info"
235+
},
236+
"column_span": {
237+
"when": {
238+
"colSpan": 2
239+
},
240+
"message": "colSpan belongs to Table.Header and Table.Cell, so it is only applied in the \"Column Span\" structure, where it routes to the first header. Add colSpan to any Table.Cell in children to span body cells, and drop the cells it covers so the row keeps the same column count.",
241+
"type": "info"
227242
}
228243
},
229244
"structureModes": {
@@ -243,6 +258,14 @@
243258
"label": "Filter wrapper",
244259
"template": "<Table{{props}}>\n{{children}}\n</Table>",
245260
"children": "<Table.Head>\n <Table.Row>\n <Table.Header text=\"Territory ID\" />\n <Table.Header text=\"Name\" />\n <Table.Header text=\"Title\" />\n <Table.Header text=\"Start Date\" textAlign=\"right\" />\n </Table.Row>\n</Table.Head>\n<Table.Body>\n <Table.Row>\n <Table.Cell text=\"1001\" numberSpacing=\"tabular\" />\n <Table.Cell text=\"Ada Lovelace\" />\n <Table.Cell text=\"Engineer\" />\n <Table.Cell text=\"Apr 9, 2026\" textAlign=\"right\" />\n </Table.Row>\n <Table.Row>\n <Table.Cell text=\"1002\" numberSpacing=\"tabular\" />\n <Table.Cell text=\"Grace Hopper\" />\n <Table.Cell text=\"Manager\" />\n <Table.Cell text=\"Apr 10, 2026\" textAlign=\"right\" />\n </Table.Row>\n</Table.Body>"
261+
},
262+
"column_span": {
263+
"label": "Column Span",
264+
"template": "<Table{{props}}>\n <Table.Head>\n <Table.Row>\n <Table.Header{{Table.Header.props}} text=\"Contact\" textAlign=\"center\" />\n <Table.Header text=\"Status\" />\n </Table.Row>\n </Table.Head>\n <Table.Body>\n{{children}}\n </Table.Body>\n</Table>",
265+
"children": " <Table.Row>\n <Table.Cell text=\"Ada Lovelace\" />\n <Table.Cell text=\"ada@example.com\" />\n <Table.Cell text=\"Active\" />\n </Table.Row>\n <Table.Row>\n <Table.Cell text=\"Grace Hopper\" />\n <Table.Cell text=\"grace@example.com\" />\n <Table.Cell text=\"Pending\" />\n </Table.Row>",
266+
"props": {
267+
"colSpan": 2
268+
}
246269
}
247270
}
248271
},
@@ -258,6 +281,11 @@
258281
]
259282
}
260283
},
284+
"propSyncOnEnable": {
285+
"colSpan": {
286+
"structureMode": "column_span"
287+
}
288+
},
261289
"codegenDefaultProps": {
262290
"size": "sm"
263291
}

playbook/app/pb_kits/playbook/pb_table/docs/_playground.overrides.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
"label": "Filter wrapper",
1717
"template": "<Table{{props}}>\n{{children}}\n</Table>",
1818
"children": "<Table.Head>\n <Table.Row>\n <Table.Header text=\"Territory ID\" />\n <Table.Header text=\"Name\" />\n <Table.Header text=\"Title\" />\n <Table.Header text=\"Start Date\" textAlign=\"right\" />\n </Table.Row>\n</Table.Head>\n<Table.Body>\n <Table.Row>\n <Table.Cell text=\"1001\" numberSpacing=\"tabular\" />\n <Table.Cell text=\"Ada Lovelace\" />\n <Table.Cell text=\"Engineer\" />\n <Table.Cell text=\"Apr 9, 2026\" textAlign=\"right\" />\n </Table.Row>\n <Table.Row>\n <Table.Cell text=\"1002\" numberSpacing=\"tabular\" />\n <Table.Cell text=\"Grace Hopper\" />\n <Table.Cell text=\"Manager\" />\n <Table.Cell text=\"Apr 10, 2026\" textAlign=\"right\" />\n </Table.Row>\n</Table.Body>"
19+
},
20+
"column_span": {
21+
"label": "Column Span",
22+
"template": "<Table{{props}}>\n <Table.Head>\n <Table.Row>\n <Table.Header{{Table.Header.props}} text=\"Contact\" textAlign=\"center\" />\n <Table.Header text=\"Status\" />\n </Table.Row>\n </Table.Head>\n <Table.Body>\n{{children}}\n </Table.Body>\n</Table>",
23+
"children": " <Table.Row>\n <Table.Cell text=\"Ada Lovelace\" />\n <Table.Cell text=\"ada@example.com\" />\n <Table.Cell text=\"Active\" />\n </Table.Row>\n <Table.Row>\n <Table.Cell text=\"Grace Hopper\" />\n <Table.Cell text=\"grace@example.com\" />\n <Table.Cell text=\"Pending\" />\n </Table.Row>",
24+
"props": {
25+
"colSpan": 2
26+
}
1927
}
2028
}
2129
},
@@ -29,6 +37,12 @@
2937
"platforms": ["react"]
3038
}
3139
},
40+
"propTargets": {
41+
"colSpan": "Table.Header.props"
42+
},
43+
"propSyncOnEnable": {
44+
"colSpan": { "structureMode": "column_span" }
45+
},
3246
"codegenDefaultProps": {
3347
"size": "sm"
3448
},
@@ -45,6 +59,10 @@
4559
"name": "Sticky Columns",
4660
"props": ["sticky", "stickyLeftColumn", "stickyRightColumn"]
4761
},
62+
{
63+
"name": "Subcomponent Props",
64+
"props": ["colSpan"]
65+
},
4866
{
4967
"name": "Filter Variant",
5068
"props": ["variant", "title", "filterContent", "filterProps", "pagination", "cardProps", "titleProps"]
@@ -174,6 +192,11 @@
174192
"presetName": "With Filter",
175193
"message": "With Filter shows the withFilter wrapper while keeping all table children editable.",
176194
"type": "info"
195+
},
196+
"column_span": {
197+
"when": { "colSpan": 2 },
198+
"message": "colSpan belongs to Table.Header and Table.Cell, so it is only applied in the \"Column Span\" structure, where it routes to the first header. Add colSpan to any Table.Cell in children to span body cells, and drop the cells it covers so the row keeps the same column count.",
199+
"type": "info"
177200
}
178201
}
179202
}

playbook/app/pb_kits/playbook/pb_table/docs/_sections.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ sections:
1414
- table_with_subcomponents
1515
- table_with_subcomponents_as_divs
1616
- table_with_background_kit
17-
17+
- table_colspan
1818

1919
- title: "Sticky & Positional Behaviors"
2020
examples:
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<%= pb_rails("table", props: { size: "sm", vertical_border: true }) do %>
2+
<%= pb_rails("table/table_head") do %>
3+
<%= pb_rails("table/table_row") do %>
4+
<%= pb_rails("table/table_header", props: { text_align: "center", colspan: 2, text: "Column 1 (spans 2 columns)"}) %>
5+
<%= pb_rails("table/table_header", props: { text: "Column 2"}) %>
6+
<%= pb_rails("table/table_header", props: { text: "Column 3"}) %>
7+
<%= pb_rails("table/table_header", props: { text: "Column 4"}) %>
8+
<% end %>
9+
<% end %>
10+
<%= pb_rails("table/table_body") do %>
11+
<%= pb_rails("table/table_row") do %>
12+
<%= pb_rails("table/table_cell", props: { text: "Value 1"}) %>
13+
<%= pb_rails("table/table_cell", props: { text: "Value 2"}) %>
14+
<%= pb_rails("table/table_cell", props: { text: "Value 3"}) %>
15+
<%= pb_rails("table/table_cell", props: { text: "Value 4"}) %>
16+
<%= pb_rails("table/table_cell", props: { text: "Value 5"}) %>
17+
<% end %>
18+
<%= pb_rails("table/table_row") do %>
19+
<%= pb_rails("table/table_cell", props: { text: "Value 1"}) %>
20+
<%= pb_rails("table/table_cell", props: { text: "Value 2"}) %>
21+
<%= pb_rails("table/table_cell", props: { text: "Value 3"}) %>
22+
<%= pb_rails("table/table_cell", props: { text: "Value 4 (spans 2 columns)", colspan: 2, text_align: "center"}) %>
23+
<% end %>
24+
<%= pb_rails("table/table_row") do %>
25+
<%= pb_rails("table/table_cell", props: { text: "Value 1"}) %>
26+
<%= pb_rails("table/table_cell", props: { text: "Value 2"}) %>
27+
<%= pb_rails("table/table_cell", props: { text: "Value 3"}) %>
28+
<%= pb_rails("table/table_cell", props: { text: "Value 4"}) %>
29+
<%= pb_rails("table/table_cell", props: { text: "Value 5"}) %>
30+
<% end %>
31+
<% end %>
32+
<% end %>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react'
2+
3+
import Table from '../_table'
4+
5+
const TableSm = (props) => {
6+
return (
7+
<Table
8+
size="sm"
9+
verticalBorder
10+
{...props}
11+
>
12+
<Table.Head>
13+
<Table.Row>
14+
<Table.Header colSpan={2}
15+
text="Column 1 (spans 2 columns)"
16+
textAlign="center"
17+
/>
18+
<Table.Header text="Column 2" />
19+
<Table.Header text="Column 3" />
20+
<Table.Header text="Column 4" />
21+
</Table.Row>
22+
</Table.Head>
23+
<Table.Body>
24+
<Table.Row>
25+
<Table.Cell text="Value 1" />
26+
<Table.Cell text="Value 2" />
27+
<Table.Cell text="Value 3" />
28+
<Table.Cell text="Value 4" />
29+
<Table.Cell text="Value 5" />
30+
</Table.Row>
31+
<Table.Row>
32+
<Table.Cell text="Value 1" />
33+
<Table.Cell text="Value 2" />
34+
<Table.Cell text="Value 3" />
35+
<Table.Cell colSpan={2}
36+
text="Value 4 (spans 2 columns)"
37+
textAlign="center"
38+
/>
39+
</Table.Row>
40+
<Table.Row>
41+
<Table.Cell text="Value 1" />
42+
<Table.Cell text="Value 2" />
43+
<Table.Cell text="Value 3" />
44+
<Table.Cell text="Value 4" />
45+
<Table.Cell text="Value 5" />
46+
</Table.Row>
47+
</Table.Body>
48+
</Table>
49+
)
50+
}
51+
52+
export default TableSm
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The sub-component Table elements `table_header` / `Table.Header` and `table_cell` / `Table.Cell` accept `colspan` / `colSpan` which takes a number to set how many columns that cell should span.
2+
3+
`colspan` / `colSpan` only applies when `tag="table"` (the default). It has no effect when `tag="div"`, since it has no equivalent in `div`-based table layouts.
4+
5+
For existing implementations passing `colspan` / `colSpan` through `htmlOptions`, this continues to work — the dedicated prop takes precedence if both are supplied.

playbook/app/pb_kits/playbook/pb_table/docs/example.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ examples:
4545
- table_with_filter_variant_external_filter_rails: Variant with Filter (External Filter)
4646
- table_with_filter_variant_with_pagination: Variant with Filter and Pagination
4747
- table_with_filter_with_card_title_props: Variant with Filter (with Card and Title Props)
48-
48+
- table_colspan: Table with Colspan
49+
4950
react:
5051
- table_sm: Small
5152
- table_md: Medium
@@ -90,3 +91,4 @@ examples:
9091
- table_with_filter_variant: Variant with Filter
9192
- table_with_filter_variant_with_pagination: Variant with Filter and Pagination
9293
- table_with_filter_with_card_title_props: Variant with Filter (with Card and Title Props)
94+
- table_colspan: Table with Colspan

playbook/app/pb_kits/playbook/pb_table/docs/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ export { default as TableWithHeaderStyleBorderless } from './_table_with_header_
4141
export { default as TableWithHeaderStyleFloating } from './_table_with_header_style_floating.jsx'
4242
export { default as TableWithFilterVariant } from './_table_with_filter_variant.jsx'
4343
export { default as TableWithFilterVariantWithPagination } from './_table_with_filter_variant_with_pagination.jsx'
44-
export { default as TableWithFilterWithCardTitleProps } from './_table_with_filter_with_card_title_props.jsx'
44+
export { default as TableWithFilterWithCardTitleProps } from './_table_with_filter_with_card_title_props.jsx'
45+
export { default as TableColspan } from './_table_colspan.jsx'

playbook/app/pb_kits/playbook/pb_table/kit.schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@
227227
],
228228
"default": false
229229
},
230+
"colSpan": {
231+
"type": "number",
232+
"platforms": [
233+
"react"
234+
]
235+
},
230236
"text": {
231237
"platforms": [
232238
"rails"

0 commit comments

Comments
 (0)