import { ControlsWithNote, DocsHeader, Footer } from '@sb/components'; import { Canvas, Markdown, Meta } from '@storybook/addon-docs/blocks'; import * as ComponentStories from './AnalyticalTable.stories'; import ColumnPropertiesMd from './ColumnProperties.md?raw';
Show shortened Code
const columns = [
{
Header: 'Name',
accessor: 'name'
},
{
Header: 'Age',
accessor: 'age'
},
{
Header: 'Friend Name',
accessor: 'friend.name'
},
{
Header: 'Friend Age',
accessor: 'friend.age'
}
];
const data = [
{
age: 80,
friend: {
age: 68,
name: 'Carver Vance'
},
name: 'Allen Best',
status: 'Positive'
},
{
age: 31,
friend: {
age: 70,
name: 'Strickland Gallegos'
},
name: 'Combs Fleming',
status: 'None'
}
// shortened for readability
];
const TableComp = () => {
return (
<AnalyticalTable
columns={columns}
data={data}
visibleRows={5}
onAutoResize={() => {}}
onColumnsReorder={() => {}}
onGroup={() => {}}
onLoadMore={() => {}}
onRowClick={() => {}}
onRowExpandChange={() => {}}
onRowSelect={() => {}}
onSort={() => {}}
onTableScroll={() => {}}
/>
);
};{ColumnPropertiesMd}
<Canvas of={ComponentStories.TreeTable} sourceState={'none'} />
The data structure of the tree table is as follows:
const data = {
name: "Greg Miller",
age: 35,
friend: {
name: "Rose Franco",
age: 32,
},
status: "None",
subRows: [
{
name: "Rick DeAngelo",
age: 25,
friend: {
name: "Susanne Franco",
age: 37,
},
status: "None",
subRows: [...],
},
],
...
};In this example the default key for sub row detection is used (subRows), you can use any key you like by setting the subRowsKey prop.
The table initially contains 50 rows, when the last 10 rows are reached the table will load more data.
Note: To prevent the table state from resetting when the data is updated, please see this recipe.
Show Code
const InfiniteScrollTable = (props) => {
const [data, setData] = useState(props.data.slice(0, 50));
const [loading, setLoading] = useState(false);
const offset = useRef(50);
const onLoadMore = () => {
setLoading(true);
};
useEffect(() => {
if (loading) {
setTimeout(() => {
setData((prev) => [...prev, ...props.data.slice(offset.current, offset.current + 50)]);
setLoading(false);
offset.current += 50;
}, 2000);
}
}, [loading, props.data, offset.current]);
return (
<AnalyticalTable
data={data}
columns={props.columns}
infiniteScroll={true}
infiniteScrollThreshold={10}
header="Scroll to load more data"
onLoadMore={onLoadMore}
loading={loading}
reactTableOptions: {{ autoResetSelectedRows: false }}
/>
);
};Adding custom subcomponents below table rows can be achieved by setting the renderRowSubComponent prop.
The prop expects a function with an optional parameter containing the row instance, there you can control which row should display subcomponents. If you want to display the subcomponent at the bottom of the row without an expandable container, you can set subComponentsBehavior prop to "Visible" or to "IncludeHeight". "Visible" simply adds the subcomponent to the row without including its height in the initial calculation of the table body, whereas "IncludeHeight" does.
- When
renderRowSubComponentis set,groupingis disabled. - When rendering active elements inside the subcomponent, make sure to add the `data-subcomponent-active-element' attribute, otherwise focus behavior won't be consistent.
- When
AnalyticalTableSubComponentsBehavior.IncludeHeightorAnalyticalTableSubComponentsBehavior.IncludeHeightExpandableis used,AnalyticalTableVisibleRowCountMode.Interactiveis not supported.
<ControlsWithNote of={ComponentStories.Subcomponents} include={['renderRowSubComponent', 'subComponentsBehavior']} />
Show Code
const TableWithSubcomponents = (props) => {
const renderRowSubComponent = (row) => {
if (row.id === '0') {
return (
<FlexBox
style={{ backgroundColor: 'lightblue', height: '300px' }}
justifyContent={FlexBoxJustifyContent.Center}
alignItems={FlexBoxAlignItems.Center}
direction={FlexBoxDirection.Column}
>
<Tag>height: 300px</Tag>
<Text>This subcomponent will only be displayed below the first row.</Text>
<hr />
<Text>
The button below is rendered with `data-subcomponent-active-element` attribute to ensure consistent focus
behavior
</Text>
<Button data-subcomponent-active-element>Click</Button>
</FlexBox>
);
}
if (row.id === '1') {
return (
<FlexBox
style={{ backgroundColor: 'lightyellow', height: '100px' }}
justifyContent={FlexBoxJustifyContent.Center}
alignItems={FlexBoxAlignItems.Center}
direction={FlexBoxDirection.Column}
>
<Tag>height: 100px</Tag>
<Text>This subcomponent will only be displayed below the second row.</Text>
</FlexBox>
);
}
if (row.id === '2') {
return null;
}
return (
<FlexBox
style={{ backgroundColor: 'lightgrey', height: '50px' }}
justifyContent={FlexBoxJustifyContent.Center}
alignItems={FlexBoxAlignItems.Center}
direction={FlexBoxDirection.Column}
>
<Tag>height: 50px</Tag>
<Text>This subcomponent will be displayed below all rows except the first, second and third.</Text>
</FlexBox>
);
};
return (
<AnalyticalTable
data={props.data}
columns={props.columns}
renderRowSubComponent={renderRowSubComponent}
subComponentsBehavior={AnalyticalTableSubComponentsBehavior.Expandable} //default value
/>
);
};By adding the visibleRowCountMode prop and setting it to AnalyticalTableVisibleRowCountMode.Auto the table automatically fills the surrounding container with rows and when setting it to AnalyticalTableVisibleRowCountMode.AutoWithEmptyRows, empty rows fill the container as well, if not enough visible rows are available.
<ControlsWithNote of={ComponentStories.DynamicRowCount} include={['containerHeight', 'visibleRowCountMode']} hideHTMLPropsNote />
const TableComponent = (props) => {
return (
<div style={{ height: `${props.containerHeight}px` }}>
<AnalyticalTable
data={props.data}
columns={props.columns}
visibleRowCountMode={AnalyticalTableVisibleRowCountMode.Auto}
// visibleRowCountMode={AnalyticalTableVisibleRowCountMode.AutoWithEmptyRows}
header={`Current height: ${props.containerHeight}px - Change the height in the table above`}
/>
</div>
);
};<ControlsWithNote of={ComponentStories.ResponsiveColumns} hideHTMLPropsNote include={['adjustTableHeightOnPopIn', 'containerWidth']} />
To responsively hide columns or move content to the first column, you can add the responsiveMinWidth column option. If
you want the column to "pop-in" the responsivePopIn has to be set to true, otherwise the column will be hidden when the
responsiveMinWidth exceeds the table width. It's also possible to change the header of the pop-in column by setting the
PopInHeader option.
Note: It is recommended to offer column options such as filtering, sorting and grouping only for columns that are always displayed.
In the example below you can have a look at this behavior:
800: The content of the "Action" column is moved to the first column (responsiveMinWidth: 801)600: The content of the "Age" column is moved to the first column (responsiveMinWidth: 601) and receives a custom header.400: The content of the "Friend Name" column is moved to the first column and the "Friend Age" column is hidden (responsiveMinWidth: 401). The "Friend Name" column also receives a custom header.
const COLUMNS = [
{
Header: 'Name',
accessor: 'name'
},
{
disableSortBy: true,
responsivePopIn: true,
responsiveMinWidth: 601,
PopInHeader: 'Custom Header Text (age)',
Header: 'Age',
accessor: 'age'
},
{
disableSortBy: true,
responsivePopIn: true,
responsiveMinWidth: 401,
Header: 'Friend Name',
PopInHeader: (instance) => {
return <div style={{ color: 'red' }}>Friend Name (custom)</div>;
},
accessor: 'friend.name'
},
{ disableSortBy: true, responsiveMinWidth: 401, Header: 'Friend Age', accessor: 'friend.age' },
{
disableSortBy: true,
responsivePopIn: true,
responsiveMinWidth: 801,
id: 'actions',
Header: 'Actions',
width: 100,
disableResizing: true,
Cell: (instance) => {
return (
<FlexBox>
<Button icon="edit" />
<Button icon="delete" />
</FlexBox>
);
}
},
{
id: 'popinDisplay',
Header: 'PopinDisplay Modes',
responsivePopIn: true,
responsiveMinWidth: 801,
popinDisplay: popinDisplay, // possible values: "Block", "Inline", "WithoutHeader"
Cell: () => {
return <Text maxLines={1}>Using popinDisplay: {popinDisplay}</Text>;
}
}
];You can change the content of the pop-in cell without mutating the original cell by using the isPopIn prop of the table instance returned by the Cell column option.
Note: The cell property of the custom Cell renderer, always returns the properties and values of the cell the "popin" cell is rendered into.
const COLUMNS = [
{
Header: 'Name',
accessor: 'name'
},
{
responsivePopIn: true,
responsiveMinWidth: 600,
id: 'col',
Header: 'Column',
Cell: ({ isPopIn, cell, value }) => {
if (isPopIn) {
// this will log the properties of the `name` cell (e.g. `cell.value` is the value of the `name` cell)
console.log(cell);
// this will always log the value of this cell (`col` cell)
console.log(value);
return 'pop-in content';
}
// this will log the properties of this cell (e.g. `cell.value` is the value of the `col` cell)
console.log(cell);
// this will always log the value of this cell (`col` cell)
console.log(value);
return 'original content';
}
}
// ...
];To display show the navigation column you need to set withNavigationHighlight to true and to mark a row as "navigated" the markNavigatedRow prop is required.
With the markNavigatedRow callback it is possible to define when and how many navigation indicators should be shown.
Click on any of the rows in the example below to display the "navigated" indicator in the navigation-column.
export const TableWithNavigationIndicators = () => {
const [selectedRow, setSelectedRow] = useState();
const onRowSelect = (e) => {
setSelectedRow(e.detail.row);
};
const markNavigatedRow = useCallback(
(row) => {
return selectedRow?.id === row.id;
},
[selectedRow]
);
return (
<AnalyticalTable
data={data}
columns={columns}
withNavigationHighlight
selectionMode={selectionMode}
markNavigatedRow={markNavigatedRow}
onRowSelect={onRowSelect}
/>
);
};It is possible to define your own filter function and filter component on each column. For this you need to customize the column option filter or add a custom filter type to the reactTableOptions.filterTypes object (for a custom filter function) and the column option Filter (for a custom filter component).
Here you can find an example using a MultiComboBox with multiple values as filter.
Show static code
const filterFn = (rows, accessor, filterValue) => {
if (filterValue.length > 0) {
return rows.filter((row) => {
const rowVal = row.values[accessor];
if (filterValue.some((item) => rowVal.includes(item))) {
return true;
}
return false;
});
}
return rows;
};
const COLUMNS = [
{
Header: 'Name',
accessor: 'name',
// either define your filter function here or set is as `reactTableOption` and pass the key as string here (see below)
filter: filterFn,
Filter: ({ column }) => {
const firstNames = ['Carl', 'Dan', 'Rose', 'Susanne'];
return (
<MultiComboBox
onSelectionChange={(e) => {
column.setFilter(e.detail.items.map((item) => item.getAttribute('text')));
}}
>
{firstNames.map((item) => {
const isSelected = column?.filterValue?.some((filterVal) => filterVal.includes(item));
return <MultiComboBoxItem text={item} key={item} selected={isSelected} />;
})}
</MultiComboBox>
);
}
},
{
Header: 'Age',
accessor: 'age'
}
];
const TableComponent = () => {
return (
<ThemeProvider>
<AnalyticalTable
columns={COLUMNS}
data={DATA}
filterable
// you can also define your function here, then you can just pass the key as string to the `filter` column option
// reactTableOptions={{
// filterTypes: {
// multiValueFilter: filterFn
// }
// }}
/>
</ThemeProvider>
);
};Show static code
function NoDataTable(props) {
const [selected, setSelected] = useState('noData');
const [filtered, setFiltered] = useState(false);
const handleChange: SegmentedButtonPropTypes['onSelectionChange'] = (e) => {
const { key } = e.detail.selectedItems[0].dataset;
setSelected(key);
if (key === 'data') {
setFiltered(false);
}
};
const handleClick: ToggleButtonPropTypes['onClick'] = (e) => {
setFiltered(!!e.target.pressed);
};
const NoDataComponent: AnalyticalTablePropTypes['NoDataComponent'] =
selected === 'noData'
? undefined
: (props) => {
return filtered ? (
<IllustratedMessage role={props.accessibleRole} name={NoFilterResults} />
) : (
<IllustratedMessage role={props.accessibleRole} name={NoDataIllustration} />
);
};
return (
<>
<SegmentedButton onSelectionChange={handleChange} accessibleName="Select data view mode">
<SegmentedButtonItem selected={selected === 'noData'} data-key="noData">
Default NoData Component
</SegmentedButtonItem>
<SegmentedButtonItem selected={selected === 'illustratedMessage'} data-key="illustratedMessage">
IllustratedMessage NoData Component
</SegmentedButtonItem>
<SegmentedButtonItem selected={selected === 'data'} data-key="data">
With Data
</SegmentedButtonItem>
</SegmentedButton>{' '}
|{' '}
<ToggleButton onClick={handleClick} pressed={filtered} disabled={selected === 'data'}>
Table filtered
</ToggleButton>
<AnalyticalTable
{...props}
data={selected === 'data' ? props.data : []}
globalFilterValue={filtered ? 'Non-existing text' : undefined}
NoDataComponent={NoDataComponent}
/>
</>
);
}The onRowContextMenu callback fires when a row is right-clicked. It provides the row and column (if the click targeted a specific cell) in e.detail. The native browser context menu is not suppressed — call e.preventDefault() in your callback to replace it with a custom menu.
This example shows two tables with products that can be moved between them via buttons or a right-click context menu.
Show Code
const productData = [
{ id: '1', product: 'Laptop Pro 15', category: 'Electronics', price: 1299 },
{ id: '2', product: 'Wireless Mouse', category: 'Accessories', price: 49 },
// ...
];
type Product = (typeof productData)[number];
const productColumns = [
{ Header: 'Product', accessor: 'product' },
{ Header: 'Category', accessor: 'category' },
{ Header: 'Price', accessor: 'price', hAlign: TextAlign.End },
];
function ContextMenuExample() {
const [availableProducts, setAvailableProducts] = useState(productData);
const [selectedProducts, setSelectedProducts] = useState<Product[]>([]);
const [checkedAvailable, setCheckedAvailable] = useState<Product[]>([]);
const [checkedSelected, setCheckedSelected] = useState<Product[]>([]);
const [menuOpen, setMenuOpen] = useState(false);
const [menuTarget, setMenuTarget] = useState<'available' | 'selected'>('available');
const [contextRow, setContextRow] = useState<Product | null>(null);
const anchorRef = useRef<HTMLDivElement>(null);
const rafId = useRef(0);
useEffect(() => {
return () => {
cancelAnimationFrame(rafId.current);
};
}, []);
const moveToSelected = (rows: Product[]) => {
const ids = new Set(rows.map((r) => r.id));
setAvailableProducts((prev) => prev.filter((p) => !ids.has(p.id)));
setSelectedProducts((prev) => [...prev, ...rows.filter((r) => !prev.some((p) => p.id === r.id))]);
setCheckedAvailable([]);
};
const moveToAvailable = (rows: Product[]) => {
const ids = new Set(rows.map((r) => r.id));
setSelectedProducts((prev) => prev.filter((p) => !ids.has(p.id)));
setAvailableProducts((prev) => [...prev, ...rows.filter((r) => !prev.some((p) => p.id === r.id))]);
setCheckedSelected([]);
};
const handleRowSelect: (
setter: typeof setCheckedAvailable
) => AnalyticalTablePropTypes['onRowSelect'] = (setter) => (e) => {
const rows = Object.values(e.detail.rowsById)
.filter((r) => e.detail.selectedRowIds[r.id])
.map((r) => r.original as Product);
setter(rows);
};
const handleContextMenu: (
target: 'available' | 'selected'
) => AnalyticalTablePropTypes['onRowContextMenu'] = (target) => (e) => {
e.preventDefault();
setContextRow(e.detail.row.original as Product);
setMenuTarget(target);
if (anchorRef.current) {
anchorRef.current.style.left = `${e.clientX}px`;
anchorRef.current.style.top = `${e.clientY}px`;
}
// Defer open so it runs after the menu's onClose from the previous right-click.
setMenuOpen(false);
rafId.current = requestAnimationFrame(() => setMenuOpen(true));
};
const handleMenuItemClick = () => {
if (!contextRow) {
return;
}
if (menuTarget === 'available') {
moveToSelected([contextRow]);
} else {
moveToAvailable([contextRow]);
}
setMenuOpen(false);
setContextRow(null);
};
return (
<>
<FlexBox alignItems={FlexBoxAlignItems.Start} style={{ gap: '0.5rem' }}>
<AnalyticalTable
header="Available Products"
columns={productColumns}
data={availableProducts}
selectionMode="Multiple"
onRowContextMenu={handleContextMenu('available')}
onRowSelect={handleRowSelect(setCheckedAvailable)}
style={{ flex: 1 }}
/>
<FlexBox
direction={FlexBoxDirection.Column}
justifyContent={FlexBoxJustifyContent.Center}
style={{ alignSelf: 'center' }}
>
<Button icon="navigation-right-arrow" onClick={() => moveToSelected(checkedAvailable)} />
<Button icon="navigation-left-arrow" onClick={() => moveToAvailable(checkedSelected)} />
</FlexBox>
<AnalyticalTable
header="Selected Products"
columns={productColumns}
data={selectedProducts}
selectionMode="Multiple"
onRowContextMenu={handleContextMenu('selected')}
onRowSelect={handleRowSelect(setCheckedSelected)}
style={{ flex: 1 }}
/>
</FlexBox>
{/* Hidden anchor for Menu positioning */}
<div
ref={anchorRef}
style={{ position: 'fixed', width: 0, height: 0, pointerEvents: 'none' }}
/>
{menuOpen && (
<Menu open opener={anchorRef.current} onClose={() => setMenuOpen(false)} onItemClick={handleMenuItemClick}>
<MenuItem
text={`Move to ${menuTarget === 'available' ? 'Selected Products' : 'Available Products'}`}
icon={menuTarget === 'available' ? 'navigation-right-arrow' : 'navigation-left-arrow'}
/>
</Menu>
)}
</>
);
}Show shortened Code
const data = [
{
age: 80,
friend: {
age: 68,
name: 'Carver Vance'
},
name: 'Allen Best',
status: 'Positive'
},
{
age: 31,
friend: {
age: 70,
name: 'Strickland Gallegos'
},
name: 'Combs Fleming',
status: 'None'
}
// shortened for readability
];
const columns = [
{
Header: 'Name',
accessor: 'name',
autoResizable: true,
headerTooltip: 'Full Name'
},
{
Header: 'Age',
accessor: 'age',
autoResizable: true,
className: 'superCustomClass',
disableFilters: false,
disableGroupBy: true,
disableSortBy: false,
hAlign: 'End'
},
{
Header: 'Friend Name',
accessor: 'friend.name',
autoResizable: true
},
{
Filter: () => {},
Header: () => {},
accessor: 'friend.age',
autoResizable: true,
filter: () => {},
hAlign: 'End',
headerLabel: 'Friend Age'
},
{
Cell: () => {},
Header: 'Actions',
accessor: '.',
cellLabel: () => {},
disableFilters: true,
disableGroupBy: true,
disableResizing: true,
disableSortBy: true,
id: 'actions',
minWidth: 100,
width: 100
}
];
const TestComp2 = () => {
return (
<AnalyticalTable
data={data}
columns={columns}
alternateRowColor
columnOrder={['friend.name', 'friend.age', 'name']}
extension={
<FlexBox justifyContent="End">
<Button accessibleName="edit" design="Transparent" icon="edit" />
</FlexBox>
}
filterable
groupable
header="Table Title"
headerRowHeight={60}
highlightField="status"
infiniteScroll
infiniteScrollThreshold={20}
loadingDelay={1000}
minRows={5}
noDataText="Custom 'noDataText' message"
overscanCountHorizontal={5}
scaleWidthMode="Smart"
selectedRowIds={{
3: true
}}
selectionBehavior="Row"
selectionMode="Single"
sortable
subRowsKey="subRows"
visibleRowCountMode="Interactive"
visibleRows={5}
withRowHighlight
onAutoResize={() => {}}
onColumnsReorder={() => {}}
onGroup={() => {}}
onLoadMore={() => {}}
onRowClick={() => {}}
onRowExpandChange={() => {}}
onRowSelect={() => {}}
onSort={() => {}}
onTableScroll={() => {}}
/>
);
};