Skip to content

feat: Table Column Groups Table#4482

Open
NathanZlion wants to merge 29 commits into
mainfrom
dev-v3-natidere-table-group
Open

feat: Table Column Groups Table#4482
NathanZlion wants to merge 29 commits into
mainfrom
dev-v3-natidere-table-group

Conversation

@NathanZlion
Copy link
Copy Markdown
Member

@NathanZlion NathanZlion commented Apr 30, 2026

Description

Add Table Column Grouping Feature support for Table Component.

Related links, issue AWSUI-9594, if available: n/a

How has this been tested?

Review checklist

The following items are to be evaluated by the author(s) and the reviewer(s).

Correctness

  • Changes include appropriate documentation updates.
  • Changes are backward-compatible if not indicated, see CONTRIBUTING.md.
  • Changes do not include unsupported browser features, see CONTRIBUTING.md.
  • Changes were manually tested for accessibility, see accessibility guidelines.

Security

Testing

  • Changes are covered with new/existing unit tests?
  • Changes are covered with new/existing integration tests?

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@NathanZlion NathanZlion changed the title chore: Add test page and interface change for table feat: Table Column Groups Table May 3, 2026
@NathanZlion NathanZlion force-pushed the dev-v3-natidere-table-group branch from 40c20ee to 84451af Compare May 4, 2026 13:42
@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

❌ Patch coverage is 98.26087% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.44%. Comparing base (f493264) to head (8a3d36a).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
src/table/thead.tsx 95.93% 5 Missing ⚠️
src/table/header-cell/group-header-cell.tsx 91.66% 2 Missing ⚠️
src/table/sticky-scrolling.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4482      +/-   ##
==========================================
+ Coverage   97.41%   97.44%   +0.02%     
==========================================
  Files         933      940       +7     
  Lines       29595    30014     +419     
  Branches    10757    10946     +189     
==========================================
+ Hits        28831    29248     +417     
- Misses        716      759      +43     
+ Partials       48        7      -41     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@NathanZlion NathanZlion force-pushed the dev-v3-natidere-table-group branch from 84451af to e9f4fd0 Compare May 4, 2026 15:59
@NathanZlion NathanZlion force-pushed the dev-v3-natidere-table-group branch from d6a98cb to 5cec8c8 Compare May 5, 2026 14:22
@NathanZlion NathanZlion marked this pull request as ready for review May 6, 2026 08:36
@NathanZlion NathanZlion requested a review from a team as a code owner May 6, 2026 08:36
@NathanZlion NathanZlion requested review from georgylobko and removed request for a team and georgylobko May 6, 2026 08:36
@NathanZlion NathanZlion marked this pull request as draft May 6, 2026 11:40
@NathanZlion NathanZlion force-pushed the dev-v3-natidere-table-group branch from 8da3b07 to 227f21b Compare May 8, 2026 00:37
columnDefinition?: TableProps.ColumnDefinition<T>;
groupDefinition?: TableProps.GroupDefinition;
parentGroupIds: string[];
rowIndex: number;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should rowIndex belong to the HeaderRow?

Comment thread src/table/column-groups/utils.ts
continue;
}
buildTreeFromColumnDisplay(item.children, nodeMap, groupNode);
if (groupNode.children.length > 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this line deserves a code comment to explain why we check for children to be non-empty. Btw, can children array include group nodes - or only column nodes? If it can include groups - then this check is not sufficient.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also, can groupNode.children include column nodes that are not visible with column display?

root: TableHeaderNode<T>
): void {
for (const col of visibleColumns) {
/* istanbul ignore next */
Copy link
Copy Markdown
Member

@pan-kot pan-kot May 12, 2026

Choose a reason for hiding this comment

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

Why do we ignore it? We once had a high-sev issue, only reproducible for cases where col.id was not set.

const nodeMap = new Map<string, TableHeaderNode<T>>();

for (const col of visibleColumns) {
if (col.id) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would add a code comment explaining why ignoring columns w/o IDs is ok

nodeMap.set(group.id, new TableHeaderNode<T>(group.id, { groupDefinition: group }));
}

// Build tree
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: The comments like "Build tree" or "Compute layout" are redundant - the corresponding code (new TableHeaderNode(...), computeSubTreeHeights(root)) is clear enough.

if (columnDisplay && columnDisplay.length > 0) {
buildTreeFromColumnDisplay(columnDisplay, nodeMap, root);
} else {
connectFlatColumns(visibleColumns, nodeMap, root);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we call this connectFlatColumns? Should we call it buildTreeFromVisibleColumns - for the symmetry with the other method? This one involves much simpler transformations, but these are transformations nevertheless.

}

const rows: HeaderRow<T>[] = Array.from(rowsMap.keys())
.sort((a, b) => a - b)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why do we perform sorting twice on the same array?


import styles from './styles.css.js';

export interface TableGroupHeaderCellProps {
Copy link
Copy Markdown
Member

@pan-kot pan-kot May 12, 2026

Choose a reason for hiding this comment

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

Can we share common props with TableHeaderCellProps?

/** When true, this cell is the rightmost child within its parent group. */
isLastChildOfGroup?: boolean;
/** Determine if the cell is the right most cell of the header */
isRightmost?: boolean;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should not call it isRightmost - as it will be left-most when using RTL direction. I suggest calling this isLastColumn - we already use this name inside table's code.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the word "column" does not quite apply here semantically - maybe we can just use "isLast"

});

// Extract only the shadow classes from the boundary subscription
/* istanbul ignore next: requires real sticky column state */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is real sticky column state? Does it require some browser behaviours?

columnGroupId?: string;
isRightmost?: boolean;
/** Additional className to merge (e.g. boundary shadow classes from a secondary sticky subscription). */
extraClassName?: string;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should call it just className - that is commonly used when we need to attach some classes from the outside.

/** Additional className to merge (e.g. boundary shadow classes from a secondary sticky subscription). */
extraClassName?: string;
/** Additional ref for boundary sticky subscription (imperatively updates shadow classes). */
extraRef?: React.RefCallback<HTMLElement>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this called extraRef? Is it not just the ref we attach to the th el?

stickyColumns: StickyColumnsModel;
columnId: PropertyKey;
getClassName: (styles: null | StickyColumnsCellState) => Record<string, boolean>;
classOnly?: boolean;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this one used?

let targetCell =
allVisibleCells.length > 0
? findClosestCellByAriaColIndex(allVisibleCells, targetAriaColIndex, delta.x)
: /* istanbul ignore next */ findTableRowCellByAriaColIndex(targetRow, targetAriaColIndex, delta.x);
Copy link
Copy Markdown
Member

@pan-kot pan-kot May 12, 2026

Choose a reason for hiding this comment

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

Why is this ignored? The keyboard nav should be testable and tested with unit tests

targetCell =
allVisibleCells.length > 0
? findClosestCellByAriaColIndex(allVisibleCells, targetAriaColIndex, delta.x)
: /* istanbul ignore next */ findTableRowCellByAriaColIndex(skipRow, targetAriaColIndex, delta.x);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same here

// Jump to the first row after this cell's span (↓) or one row before the cell's start (↑).
const skipToRowIndex = delta.y > 0 ? cellRowIndex + cellRowSpan : cellRowIndex - 1;
const skipRow = findTableRowByAriaRowIndex(this.table, skipToRowIndex, delta.y);
/* istanbul ignore next */ if (!skipRow) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same here

targetCell = findClosestCellByAriaColIndex(allVisibleCells, skipToColIndex, delta.x);
if (!targetCell || targetCell === cellElement) {
return null;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The changes in this file adds lots of complexity. What is the behaviour w/o this changes?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think there is an opportunity to significantly simplify the code here. Theoretically, the findCell by next col/row index utils should locate the correct element to focus, and might require changes to be aware of col/row spans. It is not clear why changes in the grid nav logic are necessary.

* are only in one <tr> in the DOM but visually occupy multiple rows.
*/
export function getAllCellsInRow(table: null | HTMLTableElement, targetAriaRowIndex: number): HTMLTableCellElement[] {
/* istanbul ignore next */ if (!table) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should test this, not ignore

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess the problem is that we expect table to always be there - so it is kind of hard to make it be null for this util call. In that case we might check for the table before calling the util instead.

Comment thread src/table/internal.tsx

const GRID_NAVIGATION_PAGE_SIZE = 10;
const SELECTION_COLUMN_WIDTH = 54;
const SELECTION_COLUMN_WIDTH = 40;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why did we change that?

Comment thread src/table/internal.tsx
});

// Build visible column IDs set for grouping
const visibleColumnIds = new Set(visibleColumnDefinitions.map((col, idx) => col.id || `column-${idx}`));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should use getColumnKey() here

{...getTableRoleProps({ tableRole })}
>
{hasGroupedColumns && columnDefinitions && (
<colgroup>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should we also use <TableColGroup /> here?

Comment thread src/table/thead.tsx
const { getColumnStyles, columnWidths, updateColumn, setCell } = useColumnWidths();
const { getColumnStyles, columnWidths, updateColumn, updateGroup, setCell } = useColumnWidths();

/* istanbul ignore next: resize requires real DOM measurements */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would try to find a way and cover it with unit tests to be safe

Comment thread src/table/thead.tsx
const handleSplitGroupResize = (leafIds: string[], newWidth: number) => {
const lastLeaf = leafIds[leafIds.length - 1];
if (lastLeaf) {
const currentHalfWidth = leafIds.reduce((sum, id) => sum + (columnWidths.get(id) || 120), 0);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is 120?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants