Skip to content

Latest commit

 

History

History
696 lines (601 loc) · 19.5 KB

File metadata and controls

696 lines (601 loc) · 19.5 KB

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';


Example

Code

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={() => {}}
    />
  );
};

Properties

Column Properties

{ColumnPropertiesMd}


More Examples


Tree Table

Set isTreeTable to true and provide nested data via the subRows key (configurable with subRowsKey). Each row with subRows becomes expandable.

<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.


Infinite Scrolling

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 faq entry.

Code

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 }}
    />
  );
};

Auto Row Count

Set visibleRowCountMode to AnalyticalTableVisibleRowCountMode.Auto or AutoWithEmptyRows to let the table automatically fill its container with rows.

Prerequisites:

  • The table must be placed inside a container with a defined height (e.g. height: 500px, flex: 1, or a CSS Grid row). Without a constrained parent, the table cannot determine how many rows to render.
  • AutoWithEmptyRows fills remaining space with empty rows when there aren't enough data rows to fill the container.
  • Auto may lead to inconsistent table heights depending on the container — prefer AutoWithEmptyRows for a stable layout.

<ControlsWithNote of={ComponentStories.AutoRowCount} include={['containerHeight', 'visibleRowCountMode']} hideHTMLPropsNote />

Code

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>
  );
};

Responsive Columns (Pop-In)

<ControlsWithNote of={ComponentStories.ResponsiveColumnsPopIn} 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.

Columns Config

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: AnalyticalTablePopinDisplay.Block, // possible values: "Block", "Inline", "WithoutHeader"
    Cell: () => {
      return <Text maxLines={1}>Using popinDisplay: Block</Text>;
    }
  }
];

How to change the content of the pop-in cell?

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';
    }
  }
  // ...
];

Table Without Data

Use the NoDataComponent prop to customize the empty state. By default, a simple text message is shown. You can pass a custom component (e.g. an IllustratedMessage) to display a richer empty state for different scenarios like "no data" vs. "no filter results". The component receives an accessibleRole prop that should be forwarded for accessibility.

Note: When using an IllustratedMessage as NoDataComponent, the table must have sufficient height for the illustration to render properly. With visibleRowCountMode set to Auto or AutoWithEmptyRows, the table container needs a minimum height of approximately 300px. With the default Fixed mode, ensure visibleRows provides enough vertical space (typically 5+ rows).

Code

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}
      />
    </>
  );
}

Context Menu

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.

Code

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>
      )}
    </>
  );
}

Kitchen Sink

A comprehensive example combining many AnalyticalTable features: sorting, filtering, grouping, custom cells, row and navigation highlighting, infinite scrolling, column reordering, vertical alignment, scaleWidthModeOptions for custom renderers, retainColumnWidth, sortDescFirst, and more.

Code

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',
    sortDescFirst: true
  },
  {
    Header: 'Friend Name',
    accessor: 'friend.name',
    autoResizable: true,
    vAlign: VerticalAlign.Middle
  },
  {
    Filter: () => {},
    Header: () => {},
    accessor: 'friend.age',
    autoResizable: true,
    filter: () => {},
    hAlign: 'End',
    headerLabel: 'Friend Age',
    scaleWidthModeOptions: { headerString: 'Friend Age' }
  },
  {
    Header: 'Status',
    id: 'os',
    Cell: () => {},
    scaleWidthModeOptions: { cellString: 'Negative' }
  },
  {
    Cell: () => {},
    Header: 'Actions',
    accessor: '.',
    cellLabel: () => {},
    disableFilters: true,
    disableGlobalFilter: true,
    disableGroupBy: true,
    disableResizing: true,
    disableSortBy: true,
    disableDragAndDrop: 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}
      retainColumnWidth
      scaleWidthMode="Smart"
      selectedRowIds={{
        3: true
      }}
      selectionBehavior="Row"
      selectionMode="Single"
      sortable
      subRowsKey="subRows"
      visibleRowCountMode="Interactive"
      visibleRows={5}
      withNavigationHighlight
      withRowHighlight
      onAutoResize={() => {}}
      onColumnsReorder={() => {}}
      onGroup={() => {}}
      onLoadMore={() => {}}
      onRowClick={() => {}}
      onRowExpandChange={() => {}}
      onRowSelect={() => {}}
      onSort={() => {}}
      onTableScroll={() => {}}
    />
  );
};