feat(ui): add column width configuration to Table component

Added support for configuring column widths in the Table component
using React Aria's ResizableTableContainer. Columns now accept
`width`, `defaultWidth`, `minWidth`, and `maxWidth` props.

- `width`/`defaultWidth` accept ColumnSize (number, percentage, or fr units)
- `minWidth`/`maxWidth` accept ColumnStaticSize (number or percentage)
- Wrapped Table in ResizableTableContainer to enable width system
- Updated stories to demonstrate proportional column widths

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-01-15 15:18:14 +01:00
parent 4fb15d223e
commit b01ab96638
6 changed files with 94 additions and 61 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Added support for column width configuration in Table component. Columns now accept `width`, `defaultWidth`, `minWidth`, and `maxWidth` props for responsive layout control.
Affected components: Table, Column
+9 -1
View File
@@ -7,6 +7,8 @@ import { ButtonProps as ButtonProps_2 } from 'react-aria-components';
import { CellProps as CellProps_2 } from 'react-aria-components';
import { CheckboxProps as CheckboxProps_2 } from 'react-aria-components';
import { ColumnProps as ColumnProps_2 } from 'react-aria-components';
import type { ColumnSize } from '@react-types/table';
import type { ColumnStaticSize } from '@react-types/table';
import { ComponentProps } from 'react';
import type { ComponentPropsWithRef } from 'react';
import { DetailedHTMLProps } from 'react';
@@ -478,6 +480,8 @@ export interface ColumnConfig<T extends TableItem> {
// (undocumented)
cell: (item: T) => ReactNode;
// (undocumented)
defaultWidth?: ColumnSize | null;
// (undocumented)
header?: () => ReactNode;
// (undocumented)
id: string;
@@ -490,7 +494,11 @@ export interface ColumnConfig<T extends TableItem> {
// (undocumented)
label: string;
// (undocumented)
width?: number | string;
maxWidth?: ColumnStaticSize | null;
// (undocumented)
minWidth?: ColumnStaticSize | null;
// (undocumented)
width?: ColumnSize | null;
}
// @public (undocumented)
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { Key } from 'react-aria-components';
import { type Key, ResizableTableContainer } from 'react-aria-components';
import { TableRoot } from './TableRoot';
import { TableHeader } from './TableHeader';
import { TableBody } from './TableBody';
@@ -132,67 +132,73 @@ export function Table<T extends TableItem>({
{liveRegionLabel}
</VisuallyHidden>
<TableRoot
selectionMode={selectionMode}
selectionBehavior={selectionBehavior}
selectedKeys={selectedKeys}
onSelectionChange={onSelectionChange}
sortDescriptor={sort?.descriptor ?? undefined}
onSortChange={sort?.onSortChange}
disabledKeys={disabledRows}
stale={isStale}
aria-describedby={liveRegionId}
>
<TableHeader columns={visibleColumns}>
{column =>
column.header ? (
<>{column.header()}</>
) : (
<Column
id={column.id}
isRowHeader={column.isRowHeader}
allowsSorting={column.isSortable}
>
{column.label}
</Column>
)
}
</TableHeader>
<TableBody
items={data}
renderEmptyState={
emptyState ? () => <Flex p="3">{emptyState}</Flex> : undefined
}
<ResizableTableContainer>
<TableRoot
selectionMode={selectionMode}
selectionBehavior={selectionBehavior}
selectedKeys={selectedKeys}
onSelectionChange={onSelectionChange}
sortDescriptor={sort?.descriptor ?? undefined}
onSortChange={sort?.onSortChange}
disabledKeys={disabledRows}
stale={isStale}
aria-describedby={liveRegionId}
>
{item => {
const itemIndex = data?.indexOf(item) ?? -1;
if (isRowRenderFn(rowConfig)) {
return rowConfig({
item,
index: itemIndex,
});
<TableHeader columns={visibleColumns}>
{column =>
column.header ? (
<>{column.header()}</>
) : (
<Column
id={column.id}
isRowHeader={column.isRowHeader}
allowsSorting={column.isSortable}
width={column.width}
defaultWidth={column.defaultWidth}
minWidth={column.minWidth}
maxWidth={column.maxWidth}
>
{column.label}
</Column>
)
}
</TableHeader>
<TableBody
items={data}
renderEmptyState={
emptyState ? () => <Flex p="3">{emptyState}</Flex> : undefined
}
>
{item => {
const itemIndex = data?.indexOf(item) ?? -1;
return (
<Row
id={String(item.id)}
columns={visibleColumns}
href={rowConfig?.getHref?.(item)}
onAction={
rowConfig?.onClick
? () => rowConfig?.onClick?.(item)
: undefined
}
>
{column => (
<Fragment key={column.id}>{column.cell(item)}</Fragment>
)}
</Row>
);
}}
</TableBody>
</TableRoot>
if (isRowRenderFn(rowConfig)) {
return rowConfig({
item,
index: itemIndex,
});
}
return (
<Row
id={String(item.id)}
columns={visibleColumns}
href={rowConfig?.getHref?.(item)}
onAction={
rowConfig?.onClick
? () => rowConfig?.onClick?.(item)
: undefined
}
>
{column => (
<Fragment key={column.id}>{column.cell(item)}</Fragment>
)}
</Row>
);
}}
</TableBody>
</TableRoot>
</ResizableTableContainer>
{pagination.type === 'page' && (
<TablePagination
pageSize={pagination.pageSize}
@@ -54,6 +54,7 @@ export const BasicLocalData: Story = {
id: 'name',
label: 'Name',
isRowHeader: true,
defaultWidth: '4fr',
cell: item => (
<CellText title={item.name} description={item.description} />
),
@@ -61,16 +62,19 @@ export const BasicLocalData: Story = {
{
id: 'owner',
label: 'Owner',
defaultWidth: '1fr',
cell: item => <CellText title={item.owner.name} />,
},
{
id: 'type',
label: 'Type',
defaultWidth: '1fr',
cell: item => <CellText title={item.type} />,
},
{
id: 'lifecycle',
label: 'Lifecycle',
defaultWidth: '1fr',
cell: item => <CellText title={item.lifecycle} />,
},
];
@@ -40,6 +40,7 @@ export const TableRockBand: Story = {
id: 'name',
label: 'Band name',
isRowHeader: true,
defaultWidth: '4fr',
cell: item => (
<CellProfile name={item.name} src={item.image} href={item.website} />
),
@@ -47,16 +48,19 @@ export const TableRockBand: Story = {
{
id: 'genre',
label: 'Genre',
defaultWidth: '4fr',
cell: item => <CellText title={item.genre} />,
},
{
id: 'yearFormed',
label: 'Year formed',
defaultWidth: '1fr',
cell: item => <CellText title={item.yearFormed.toString()} />,
},
{
id: 'albums',
label: 'Albums',
defaultWidth: '1fr',
cell: item => <CellText title={item.albums.toString()} />,
},
];
+5 -1
View File
@@ -21,6 +21,7 @@ import {
} from 'react-aria-components';
import type { ReactNode } from 'react';
import type { SortDescriptor as ReactStatelySortDescriptor } from 'react-stately';
import type { ColumnSize, ColumnStaticSize } from '@react-types/table';
import type { TextColors } from '../../types';
import { TablePaginationProps } from '../TablePagination';
@@ -92,7 +93,10 @@ export interface ColumnConfig<T extends TableItem> {
header?: () => ReactNode;
isSortable?: boolean;
isHidden?: boolean;
width?: number | string;
width?: ColumnSize | null;
defaultWidth?: ColumnSize | null;
minWidth?: ColumnStaticSize | null;
maxWidth?: ColumnStaticSize | null;
isRowHeader?: boolean;
}