Skip to content

fix: add colspan and rowspan support#79

Open
ZehranurC wants to merge 7 commits into
mainfrom
feature/table-colspan-rowspan-implementation
Open

fix: add colspan and rowspan support#79
ZehranurC wants to merge 7 commits into
mainfrom
feature/table-colspan-rowspan-implementation

Conversation

@ZehranurC

@ZehranurC ZehranurC commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Description

Add colspan and rowspan configuration to table columns with proper cell rendering and pagination safety.

Type of Change

  • 🆕 New Component
  • ✨ Feature / Enhancement
  • 🐛 Bug Fix
  • 💥 Breaking Change
  • 📦 Build / Dependency / Docs Update
  • 🧹 Code Refactoring

Screenshots / Preview

image image

Checklist

  • Self-reviewed, no leftover logs or debug code
  • Uses design tokens — no hardcoded style values
  • Accessible (keyboard nav, ARIA, contrast)
  • Tests added/updated and passing
  • Storybook story added/updated

How to Test

[#EDKUI-1410]

) : (
visibleRows.map((row, index) => <DataRow key={row.motifIndex} rowNumberStatic={index + 1} row={row} />)
visibleRows.map((row, index) => (
<DataRow key={row.motifIndex} rowNumberStatic={index + 1} row={row} spannedCellsMap={spannedCellsMap} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that we use index + 1 each time for rowNumber, so I am wondering for example how am I going to show second row to user? For example,

Image Image

I didn't manage to show Santina, Isac, Antone at the table only the first row appears.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For empty row at least filled one should match with the rowSpan I guess,

Image Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be fixed after my fixes, but I'm not sure because I couldn't see the full data content. Can you verify?

@ZehranurC ZehranurC changed the title feat(Table): add colspan and rowspan support fix: add colspan and rowspan support Jun 5, 2026

@aktasmehmet aktasmehmet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my review v1 :) There are some issues to fix. Then I'll check later.

This PR is so so so important because it directly manipulates the performance related codes of the component. We'll probably need more reviewers here.

Comment thread src/lib/components/Table/components/Body/TableBody.tsx Outdated
footer?: Footer;
width?: string;
filter?: boolean;
colSpan?: number | ((rowData: object) => number);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why need numbertype here? If it is to apply colspan for every cell, then user can easily reduce the number of colums, so I dont see a need here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two different needs:

  1. Static span (colSpan: 2) — fixed across all rows, number type is sufficient
  2. Dynamic span (colSpan: (row) => row.merged ? 2 : 1) — varies per row, function type is required

Modifying the data is incorrect because:

  • Data integrity breaks (missing rows become null/undefined)
  • Sorting/filtering breaks
  • Component dataKey stops working

example

// 4 rows of data, but Department column calculates rowSpan dynamically per group
const data = [
  { department: "Engineering", employee: "John" },  // rowSpan: 3
  { department: "Engineering", employee: "Jane" },  // (skip)
  { department: "Engineering", employee: "Bob" },   // (skip)
  { department: "Sales", employee: "Alice" }        // rowSpan: 1
];

// Halving the data makes no sense — grouping is a UI concern, not a data concern
rowSpan: (row) => row.firstInGroup ? groupSizes[row.department] : 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont find useful information in this answer. The example shows the usage of the function, not the number.

I wonder that, what would be the result for the code below:

<Table
        data={[
          { name: "Alice", age: 28, group: "A" },
          { name: "Bob", age: 34, group: "B" },
          { name: "Charlie", age: 29, group: "A" },
          { name: "Dave", age: 42, group: "B" },
        ]}
        columns={[
          { title: "Group", dataKey: "group", rowSpan: 2 },
          { title: "Name", dataKey: "name" },
          { title: "Age", dataKey: "age" },
        ]}
        border="cellBorders"
      />

Afaik, this would render only Alice and Charline lines, right? So this will show 1st row and 3rd row and so on... So, what I mean is that, user can filter the data himself without using rowspan? This will not effect the filtering also?

Am I missing something?

@ZehranurC ZehranurC Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be misunderstood.
rowSpan doesn't filter. it only determines how cells are displayed.

If you set rowSpan: 2, the Group cell spans 2 rows. But which rows appear depends on your data.

// Pass this data:

data={[
  { name: "Alice", age: 28, group: "A" },
  { name: "Charlie", age: 29, group: "A" },  // 2 people = rowSpan: 2 works fine
  { name: "Bob", age: 34, group: "B" },
  { name: "Dave", age: 42, group: "B" },
]}

// Result: Group A cell spans 2 rows ✓
If Group A has 3 people, rowSpan: 2 won't be enough. Then use dynamic rowSpan:

rowSpan: (row) => row.firstInGroup ? groupSize[row.group] : 1

It has nothing to do with filtering — rowSpan just decides how to merge cells in the rows that are already shown.

Comment thread src/lib/components/Table/types.ts
Comment thread src/lib/components/Table/components/Head/FilterColumnsRow.tsx Outdated
Comment thread src/lib/components/Table/helper.ts Outdated
Comment thread src/lib/components/Table/cellSpan.ts Outdated
Comment thread src/lib/components/Table/cellSpan.ts Outdated
const cellKey: SpannedCellKey = `${row.motifIndex}-${colIndex}`;
if (map.has(cellKey)) return;

const remainingRowsCount = rows.filter(r => r.motifIndex >= row.motifIndex).length;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another and more dangerous problem here is that since it uses original index order, this sill clamp all rowSpan to 1 after any sort!

Just check the ss

Image

here is the data to test:

<Table
        data={[
          { basarili: "Evet", nasil: "İyi", sonuc: "Mutlu" },
          { basarili: "Hayır", nasil: "Kötü", sonuc: "Mutsuz" },
          { basarili: "Evet", nasil: "Orta", sonuc: "Kararsız" },
          { basarili: "Belki", nasil: "Eh İşte", sonuc: "Belirsiz" },
          { basarili: "Hayır", nasil: "Çok Kötü", sonuc: "Çok Mutsuz" },
          { basarili: "Evet", nasil: "Mükemmel", sonuc: "Çok Mutlu" },
          { basarili: "Belki", nasil: "Fena Değil", sonuc: "Kararsız" },
          { basarili: "Hayır", nasil: "Berbat", sonuc: "Çok Mutsuz" },
          { basarili: "Evet", nasil: "Harika", sonuc: "Çok Mutlu" },
          { basarili: "Belki", nasil: "Orta", sonuc: "Kararsız" },
        ]}
        columns={[
          { dataKey: "basarili", title: "Başarılı", colSpan: obj => (obj.basarili === "Evet" ? 2 : 1) },
          { dataKey: "nasil", title: "Nasıl" },
          { dataKey: "sonuc", title: "Sonuç", rowSpan: obj => (obj.sonuc.includes("Mutlu") ? 2 : 1), sorting:{} },
        ]}
      />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be fixed after my fixes. You can review them.

Comment thread src/lib/components/Table/components/Foot/ColumnFootArea.tsx Outdated
Comment thread src/lib/components/Table/cellSpan.ts Outdated
Comment thread src/lib/components/Table/helper.ts Outdated
Comment on lines +7 to +19
export type ResolvedCellSpan = {
colSpan: number;
rowSpan: number;
};

export type SpannedCellKey = `${number}-${number}`;
export type SpannedCellsMap = Map<SpannedCellKey, ResolvedCellSpan | undefined>;

export type RenderableColumn = {
column: Column;
index: number;
colSpan?: number;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a types.ts file, so it would be better to move all these types there and access types from a single file for easy management.

@ZehranurC ZehranurC Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been fixed in the latest commit. You can verify it.

Comment thread src/lib/components/Table/components/Head/FilterColumnsRow.tsx Outdated
footer?: Footer;
width?: string;
filter?: boolean;
colSpan?: number | ((rowData: object) => number);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont find useful information in this answer. The example shows the usage of the function, not the number.

I wonder that, what would be the result for the code below:

<Table
        data={[
          { name: "Alice", age: 28, group: "A" },
          { name: "Bob", age: 34, group: "B" },
          { name: "Charlie", age: 29, group: "A" },
          { name: "Dave", age: 42, group: "B" },
        ]}
        columns={[
          { title: "Group", dataKey: "group", rowSpan: 2 },
          { title: "Name", dataKey: "name" },
          { title: "Age", dataKey: "age" },
        ]}
        border="cellBorders"
      />

Afaik, this would render only Alice and Charline lines, right? So this will show 1st row and 3rd row and so on... So, what I mean is that, user can filter the data himself without using rowspan? This will not effect the filtering also?

Am I missing something?

Comment thread src/lib/components/Table/types.ts

const ColumnFootArea = ({ background, customFooter }: Props) => {
const { originalRows, columns, showFixedRowNumbers, selectable, numberOfVisibleColumns } = useContext(TableContext);
const { usableRows, columns, showFixedRowNumbers, selectable, numberOfVisibleColumns } = useContext(TableContext);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switching from originalRowsto usableRows causes the avg footer to be shown as NaN when all rows are filtered out. You may filter with "x" Please check the ss.

Here is the sample test:

<Table
        data={[
          { name: "Alice", score: 90 },
          { name: "Bob", score: 80 },
          { name: "Carol", score: 70 },
        ]}
        columns={[
          { title: "Name", dataKey: "name", filter: true },
          { title: "Score", dataKey: "score", footer: { type: "avg" } },
        ]}
        border="cellBorders"
      />
Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be fixed in the latest commit. You can verify it.


const ColumnFootArea = ({ background, customFooter }: Props) => {
const { originalRows, columns, showFixedRowNumbers, selectable, numberOfVisibleColumns } = useContext(TableContext);
const { usableRows, columns, showFixedRowNumbers, selectable, numberOfVisibleColumns } = useContext(TableContext);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now saw that this also breaks the filtered total. Previously, it was showin the total of the whole table but now, it only shows the filtered ones as total. This is not expected.

Here is the code (Sum should stay as 600):

<Table
        data={[
          { name: "Alice", amount: 100 },
          { name: "Bob", amount: 200 },
          { name: "Carol", amount: 300 },
        ]}
        columns={[
          { title: "Name", dataKey: "name", filter: true },
          { title: "Amount", dataKey: "amount", footer: { type: "sum" } },
        ]}
        border="cellBorders"
      />
Image

@ZehranurC ZehranurC Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be fixed in the latest commit. You can verify it.

);
});
return (
<th key={"foot_" + column.dataKey} {...(actualColSpan > 1 && { colSpan: actualColSpan })}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change will lead to duplicate key error. Here is the scenario:

When :

selectable=true
AND
showFixedRowNumbers=true
AND
any data column has footer defined

--> columnsConsideringExtraCols prepends {title:''} and {title:'#'}, both with dataKey=undefined. key={'foot_' + column.dataKey} evaluates to 'foot_undefined' for both.

So this will cause the error:

<Table
        data={[
          { name: "Alice", amount: 100 },
          { name: "Bob", amount: 200 },
          { name: "Carol", amount: 300 },
        ]}
        columns={[
          { title: "Name", dataKey: "name" },
          { title: "Amount", dataKey: "amount", footer: { type: "sum" } },
        ]}
        selectable
        showFixedRowNumbers
        border="cellBorders"
      />
Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been fixed in the latest commit. You can verify it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants