@wallarm-org/design-system 0.38.2 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,33 @@
1
- import { jsx } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { TestIdProvider } from "../../utils/testId.js";
3
3
  import { TableProvider } from "./TableContext/index.js";
4
+ import { TableImperativeBridge } from "./TableImperativeBridge.js";
4
5
  import { TableInner } from "./TableInner/index.js";
5
6
  const Table = (props)=>{
6
- const { data, isLoading = false, children, className, 'aria-label': ariaLabel, 'data-testid': testId, ...providerProps } = props;
7
+ const { data, isLoading = false, children, className, 'aria-label': ariaLabel, 'data-testid': testId, ref, ...providerProps } = props;
7
8
  const isEmpty = 0 === data.length && !isLoading;
8
9
  const showSettings = !!props.onColumnVisibilityChange || !!props.onColumnOrderChange;
9
- return /*#__PURE__*/ jsx(TableProvider, {
10
+ return /*#__PURE__*/ jsxs(TableProvider, {
10
11
  data: data,
11
12
  isLoading: isLoading,
12
13
  ...providerProps,
13
- children: /*#__PURE__*/ jsx(TestIdProvider, {
14
- value: testId,
15
- children: /*#__PURE__*/ jsx(TableInner, {
16
- isEmpty: isEmpty,
17
- virtualized: props.virtualized,
18
- showSettings: showSettings,
19
- ariaLabel: ariaLabel,
20
- className: className,
21
- "data-testid": testId,
22
- children: children
14
+ children: [
15
+ ref ? /*#__PURE__*/ jsx(TableImperativeBridge, {
16
+ handleRef: ref
17
+ }) : null,
18
+ /*#__PURE__*/ jsx(TestIdProvider, {
19
+ value: testId,
20
+ children: /*#__PURE__*/ jsx(TableInner, {
21
+ isEmpty: isEmpty,
22
+ virtualized: props.virtualized,
23
+ showSettings: showSettings,
24
+ ariaLabel: ariaLabel,
25
+ className: className,
26
+ "data-testid": testId,
27
+ children: children
28
+ })
23
29
  })
24
- })
30
+ ]
25
31
  });
26
32
  };
27
33
  Table.displayName = 'Table';
@@ -7,7 +7,7 @@ import { TableRow } from "../TableRow.js";
7
7
  import { TableBodyVirtualizedContainer } from "./TableBodyVirtualizedContainer.js";
8
8
  import { TableBodyVirtualizedWindow } from "./TableBodyVirtualizedWindow.js";
9
9
  const TableBody = ()=>{
10
- const { table, isLoading, virtualized } = useTableContext();
10
+ const { table, isLoading, virtualized, tbodyRef, virtualizerRef } = useTableContext();
11
11
  const testId = useTestId('body');
12
12
  const rows = table.getRowModel().rows;
13
13
  const hasData = rows.length > 0;
@@ -15,7 +15,9 @@ const TableBody = ()=>{
15
15
  if ('window' === virtualized) return /*#__PURE__*/ jsx(TableBodyVirtualizedWindow, {});
16
16
  return /*#__PURE__*/ jsx(TableBodyVirtualizedContainer, {});
17
17
  }
18
+ virtualizerRef.current = null;
18
19
  return /*#__PURE__*/ jsxs(TBody, {
20
+ ref: tbodyRef,
19
21
  "data-testid": testId,
20
22
  children: [
21
23
  rows.map((row)=>/*#__PURE__*/ jsx(TableRow, {
@@ -1,5 +1,5 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { useCallback, useRef } from "react";
2
+ import { useCallback, useEffect } from "react";
3
3
  import { useVirtualizer } from "@tanstack/react-virtual";
4
4
  import { TABLE_VIRTUALIZATION_OVERSCAN } from "../lib/index.js";
5
5
  import { useTableContext } from "../TableContext/index.js";
@@ -7,15 +7,22 @@ import { TableBodyVirtualizedCore } from "./TableBodyVirtualizedCore.js";
7
7
  import { useResetVirtualizerOnDataChange } from "./useResetVirtualizerOnDataChange.js";
8
8
  import { useSmoothScrollOnSort } from "./useSmoothScrollOnSort.js";
9
9
  const TableBodyVirtualizedContainer = ()=>{
10
- const { table, estimateRowHeight, overscan } = useTableContext();
11
- const tbodyRef = useRef(null);
12
- const getScrollElement = useCallback(()=>tbodyRef.current?.closest('[data-table-scroll-container]') ?? null, []);
10
+ const { table, estimateRowHeight, overscan, tbodyRef, virtualizerRef } = useTableContext();
11
+ const getScrollElement = useCallback(()=>tbodyRef.current?.closest('[data-table-scroll-container]') ?? null, [
12
+ tbodyRef
13
+ ]);
13
14
  const virtualizer = useVirtualizer({
14
15
  count: table.getRowModel().rows.length,
15
16
  getScrollElement,
16
17
  estimateSize: estimateRowHeight ?? (()=>40),
17
18
  overscan: overscan ?? TABLE_VIRTUALIZATION_OVERSCAN
18
19
  });
20
+ virtualizerRef.current = virtualizer;
21
+ useEffect(()=>()=>{
22
+ virtualizerRef.current = null;
23
+ }, [
24
+ virtualizerRef
25
+ ]);
19
26
  useResetVirtualizerOnDataChange(table, virtualizer);
20
27
  useSmoothScrollOnSort(table, getScrollElement);
21
28
  return /*#__PURE__*/ jsx(TableBodyVirtualizedCore, {
@@ -1,5 +1,5 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { useCallback, useRef } from "react";
2
+ import { useCallback, useEffect } from "react";
3
3
  import { useWindowVirtualizer } from "@tanstack/react-virtual";
4
4
  import { TABLE_VIRTUALIZATION_OVERSCAN } from "../lib/index.js";
5
5
  import { useTableContext } from "../TableContext/index.js";
@@ -8,14 +8,19 @@ import { TableBodyVirtualizedCore } from "./TableBodyVirtualizedCore.js";
8
8
  import { useResetVirtualizerOnDataChange } from "./useResetVirtualizerOnDataChange.js";
9
9
  import { useSmoothScrollOnSort } from "./useSmoothScrollOnSort.js";
10
10
  const TableBodyVirtualizedWindow = ()=>{
11
- const { table, estimateRowHeight, overscan } = useTableContext();
12
- const tbodyRef = useRef(null);
11
+ const { table, estimateRowHeight, overscan, tbodyRef, virtualizerRef } = useTableContext();
13
12
  const virtualizer = useWindowVirtualizer({
14
13
  count: table.getRowModel().rows.length,
15
14
  estimateSize: estimateRowHeight ?? (()=>40),
16
15
  overscan: overscan ?? TABLE_VIRTUALIZATION_OVERSCAN,
17
16
  scrollMargin: tbodyRef.current ? getDocumentOffsetTop(tbodyRef.current) : 0
18
17
  });
18
+ virtualizerRef.current = virtualizer;
19
+ useEffect(()=>()=>{
20
+ virtualizerRef.current = null;
21
+ }, [
22
+ virtualizerRef
23
+ ]);
19
24
  useResetVirtualizerOnDataChange(table, virtualizer);
20
25
  const getScrollTarget = useCallback(()=>window, []);
21
26
  useSmoothScrollOnSort(table, getScrollTarget);
@@ -183,6 +183,8 @@ const TableProvider = (props)=>{
183
183
  const lastSelectedRowIndexRef = useRef(null);
184
184
  const theadRef = useRef(null);
185
185
  const containerRef = useRef(null);
186
+ const tbodyRef = useRef(null);
187
+ const virtualizerRef = useRef(null);
186
188
  const contextValue = useMemo(()=>({
187
189
  table,
188
190
  isLoading,
@@ -208,6 +210,8 @@ const TableProvider = (props)=>{
208
210
  lastSelectedRowIndexRef,
209
211
  theadRef,
210
212
  containerRef,
213
+ tbodyRef,
214
+ virtualizerRef,
211
215
  onEndReached,
212
216
  onEndReachedThreshold,
213
217
  onMasterCellClick,
@@ -1,6 +1,13 @@
1
1
  import type { ReactNode, RefObject } from 'react';
2
2
  import type { Column, Row, Table as TanStackTable, VisibilityState } from '@tanstack/react-table';
3
+ import type { Virtualizer } from '@tanstack/react-virtual';
3
4
  import type { TableProps, TableVirtualized } from '../types';
5
+ /**
6
+ * Union covers both virtualizer flavors used by the Table (`useVirtualizer`
7
+ * with HTMLElement scroll container and `useWindowVirtualizer` with Window).
8
+ * Lives behind `TableHandle` — consumers never touch this directly.
9
+ */
10
+ export type TableVirtualizerInstance = Virtualizer<Window, Element> | Virtualizer<HTMLElement, Element> | Virtualizer<Element, Element>;
4
11
  export interface TableContextValue<T> {
5
12
  table: TanStackTable<T>;
6
13
  isLoading: boolean;
@@ -26,6 +33,8 @@ export interface TableContextValue<T> {
26
33
  masterColumnId: string | null;
27
34
  theadRef: RefObject<HTMLTableSectionElement | null>;
28
35
  containerRef: RefObject<HTMLDivElement | null>;
36
+ tbodyRef: RefObject<HTMLTableSectionElement | null>;
37
+ virtualizerRef: RefObject<TableVirtualizerInstance | null>;
29
38
  onEndReached?: () => void;
30
39
  onEndReachedThreshold?: number;
31
40
  onMasterCellClick?: (rowId: string) => void;
@@ -0,0 +1,15 @@
1
+ import { type Ref } from 'react';
2
+ import type { TableHandle } from './types';
3
+ interface TableImperativeBridgeProps {
4
+ handleRef: Ref<TableHandle>;
5
+ }
6
+ /**
7
+ * Bridges the imperative ref forwarded to `<Table>` to context-backed
8
+ * refs (`virtualizerRef`, `tbodyRef`). Mounted inside `TableProvider` so
9
+ * it has access to the context; renders nothing.
10
+ */
11
+ export declare const TableImperativeBridge: {
12
+ ({ handleRef }: TableImperativeBridgeProps): null;
13
+ displayName: string;
14
+ };
15
+ export {};
@@ -0,0 +1,48 @@
1
+ import { useImperativeHandle } from "react";
2
+ import { useTableContext } from "./TableContext/index.js";
3
+ const resolveScrollIntoViewBlock = (align)=>{
4
+ switch(align){
5
+ case 'start':
6
+ return 'start';
7
+ case 'center':
8
+ return 'center';
9
+ case 'end':
10
+ return 'end';
11
+ default:
12
+ return 'nearest';
13
+ }
14
+ };
15
+ const TableImperativeBridge = ({ handleRef })=>{
16
+ const { table, virtualizerRef, tbodyRef } = useTableContext();
17
+ useImperativeHandle(handleRef, ()=>({
18
+ scrollToRow (id, opts = {}) {
19
+ const rows = table.getRowModel().rows;
20
+ const index = rows.findIndex((r)=>r.id === id);
21
+ if (index < 0) return false;
22
+ const virtualizer = virtualizerRef.current;
23
+ if (virtualizer) {
24
+ virtualizer.scrollToIndex(index, {
25
+ align: opts.align ?? 'auto',
26
+ behavior: opts.behavior
27
+ });
28
+ return true;
29
+ }
30
+ const tbody = tbodyRef.current;
31
+ if (!tbody) return false;
32
+ const el = tbody.querySelector(`[data-row-id="${CSS.escape(id)}"]`);
33
+ if (!(el instanceof HTMLElement)) return false;
34
+ el.scrollIntoView({
35
+ block: resolveScrollIntoViewBlock(opts.align),
36
+ behavior: opts.behavior
37
+ });
38
+ return true;
39
+ }
40
+ }), [
41
+ table,
42
+ virtualizerRef,
43
+ tbodyRef
44
+ ]);
45
+ return null;
46
+ };
47
+ TableImperativeBridge.displayName = 'TableImperativeBridge';
48
+ export { TableImperativeBridge };
@@ -27,6 +27,7 @@ const TableRowInner = ({ row, ref, 'data-index': dataIndex })=>{
27
27
  /*#__PURE__*/ jsxs(Tr, {
28
28
  ref: ref,
29
29
  "data-index": dataIndex,
30
+ "data-row-id": row.id,
30
31
  "data-testid": testId,
31
32
  className: "group/row",
32
33
  "data-selected": isSelected || void 0,
@@ -62,6 +63,7 @@ const TableRowInner = ({ row, ref, 'data-index': dataIndex })=>{
62
63
  /*#__PURE__*/ jsx(Tr, {
63
64
  ref: ref,
64
65
  "data-index": dataIndex,
66
+ "data-row-id": row.id,
65
67
  "data-testid": testId,
66
68
  className: "group/row",
67
69
  "data-selected": isSelected || void 0,
@@ -4,4 +4,4 @@ export { Table } from './Table';
4
4
  export { TableActionBar } from './TableActionBar';
5
5
  export { TableEmptyState } from './TableEmptyState';
6
6
  export { TableSettingsMenu } from './TableSettingsMenu';
7
- export type { TableAccessorColumnDef, TableCellContext, TableColumnBase, TableColumnDef, TableColumnMeta, TableColumnPinningState, TableColumnSizingState, TableDisplayColumnDef, TableExpandedState, TableGroupingState, TableOnChangeFn, TableProps, TableRow, TableRowSelectionState, TableSortingState, TableUpdater, TableVirtualized, TableVisibilityState, } from './types';
7
+ export type { TableAccessorColumnDef, TableCellContext, TableColumnBase, TableColumnDef, TableColumnMeta, TableColumnPinningState, TableColumnSizingState, TableDisplayColumnDef, TableExpandedState, TableGroupingState, TableHandle, TableOnChangeFn, TableProps, TableRow, TableRowSelectionState, TableScrollToRowOptions, TableSortingState, TableUpdater, TableVirtualized, TableVisibilityState, } from './types';
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import type { ReactNode, Ref } from 'react';
2
2
  import type { Row, RowData } from '@tanstack/react-table';
3
3
  import type { TestableProps } from '../../utils/testId';
4
4
  declare module '@tanstack/react-table' {
@@ -44,6 +44,29 @@ export type TableGroupingState = string[];
44
44
  export type TableVisibilityState = Record<string, boolean>;
45
45
  /** State updater — value or functional update */
46
46
  export type TableUpdater<T> = T | ((prev: T) => T);
47
+ /** Options for the imperative `scrollToRow` method */
48
+ export interface TableScrollToRowOptions {
49
+ /** Alignment within the viewport. Default: 'auto'. */
50
+ align?: 'start' | 'center' | 'end' | 'auto';
51
+ /** Scroll behavior. Default: 'auto'. */
52
+ behavior?: 'auto' | 'smooth';
53
+ }
54
+ /**
55
+ * Imperative handle exposed via `ref` on `<Table>`. The only supported way
56
+ * to programmatically scroll the table to a row that may be outside the
57
+ * currently rendered virtual window.
58
+ */
59
+ export interface TableHandle {
60
+ /**
61
+ * Scrolls to the row with the given id.
62
+ *
63
+ * Returns `true` if the row was found in the current row model and a
64
+ * scroll was initiated. Returns `false` if the id is not in the current
65
+ * rows or the virtualizer has not yet mounted — the caller decides
66
+ * whether to load more pages or retry on the next frame.
67
+ */
68
+ scrollToRow(id: string, opts?: TableScrollToRowOptions): boolean;
69
+ }
47
70
  /** onChange callback */
48
71
  export type TableOnChangeFn<T> = (updaterOrValue: TableUpdater<T>) => void;
49
72
  /** Public row interface — structural subset of TanStack Row<T> */
@@ -164,4 +187,6 @@ export interface TableProps<T> extends TestableProps {
164
187
  onMasterCellClick?: (rowId: string) => void;
165
188
  /** ID of the currently active (highlighted) row, or null. Controls row highlighting via data-preview-active attribute. */
166
189
  activeRowId?: string | null;
190
+ /** Imperative handle — exposes `scrollToRow(id, opts)`. See {@link TableHandle}. */
191
+ ref?: Ref<TableHandle>;
167
192
  }
package/dist/index.d.ts CHANGED
@@ -56,7 +56,7 @@ export { Skeleton, type SkeletonProps } from './components/Skeleton';
56
56
  export { SplitButton, type SplitButtonProps } from './components/SplitButton';
57
57
  export { HStack, type HStackProps, Stack, type StackProps, VStack, type VStackProps, } from './components/Stack';
58
58
  export { Switch, SwitchControl, SwitchDescription, type SwitchDescriptionProps, SwitchLabel, type SwitchLabelProps, type SwitchProps, } from './components/Switch';
59
- export { createTableColumnHelper, Table, type TableAccessorColumnDef, TableActionBar, type TableCellContext, type TableColumnBase, type TableColumnDef, type TableColumnHelper, type TableColumnMeta, type TableColumnPinningState, type TableColumnSizingState, type TableDisplayColumnDef, TableEmptyState, type TableExpandedState, type TableGroupingState, type TableOnChangeFn, type TableProps, type TableRow, type TableRowSelectionState, TableSettingsMenu, type TableSortingState, type TableUpdater, type TableVisibilityState, } from './components/Table';
59
+ export { createTableColumnHelper, Table, type TableAccessorColumnDef, TableActionBar, type TableCellContext, type TableColumnBase, type TableColumnDef, type TableColumnHelper, type TableColumnMeta, type TableColumnPinningState, type TableColumnSizingState, type TableDisplayColumnDef, TableEmptyState, type TableExpandedState, type TableGroupingState, type TableHandle, type TableOnChangeFn, type TableProps, type TableRow, type TableRowSelectionState, type TableScrollToRowOptions, TableSettingsMenu, type TableSortingState, type TableUpdater, type TableVisibilityState, } from './components/Table';
60
60
  export { Tabs, TabsButton, TabsContent, TabsLineActions, TabsList, TabsSeparator, TabsTrigger, } from './components/Tabs';
61
61
  export { Tag, TagClose, type TagProps } from './components/Tag';
62
62
  export { Text, type TextProps } from './components/Text';
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.38.1",
3
- "generatedAt": "2026-05-18T12:15:53.965Z",
2
+ "version": "0.38.2",
3
+ "generatedAt": "2026-05-18T13:33:44.697Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -42632,6 +42632,10 @@
42632
42632
  "name": "WindowVirtualization",
42633
42633
  "code": "() => {\n const largeData = useMemo(() => createLargeSecurityEvents(1000), []);\n const [columnOrder, setColumnOrder] = useState<string[]>([]);\n const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});\n\n return (\n <Table\n data={largeData}\n columns={securityColumns}\n getRowId={row => row.id}\n virtualized='window'\n columnOrder={columnOrder}\n onColumnOrderChange={setColumnOrder}\n columnSizing={columnSizing}\n onColumnSizingChange={setColumnSizing}\n />\n );\n}"
42634
42634
  },
42635
+ {
42636
+ "name": "ScrollToRow",
42637
+ "code": "() => {\n const largeData = useMemo(() => createLargeSecurityEvents(1000), []);\n const tableRef = useRef<TableHandle>(null);\n const [lastResult, setLastResult] = useState<string | null>(null);\n\n const scroll = (index: number, align: 'start' | 'center' | 'end' | 'auto' = 'center') => {\n const row = largeData[index];\n if (!row) return;\n const ok = tableRef.current?.scrollToRow(row.id, { align, behavior: 'smooth' }) ?? false;\n setLastResult(`scrollToRow(\"${row.id}\", { align: \"${align}\" }) → ${ok}`);\n };\n\n return (\n <VStack gap='md'>\n <HStack gap='sm'>\n <Button onClick={() => scroll(0, 'start')}>Top</Button>\n <Button onClick={() => scroll(499, 'center')}>Middle</Button>\n <Button onClick={() => scroll(999, 'end')}>Bottom</Button>\n <Button\n variant='ghost'\n onClick={() => {\n const ok = tableRef.current?.scrollToRow('does-not-exist') ?? false;\n setLastResult(`scrollToRow(\"does-not-exist\") → ${ok}`);\n }}\n >\n Missing id (returns false)\n </Button>\n </HStack>\n {lastResult && (\n <Text size='sm' color='secondary'>\n {lastResult}\n </Text>\n )}\n <Table\n ref={tableRef}\n className='h-500'\n data={largeData}\n columns={securityColumns}\n getRowId={row => row.id}\n virtualized='container'\n />\n </VStack>\n );\n}"
42638
+ },
42635
42639
  {
42636
42640
  "name": "InfiniteScroll",
42637
42641
  "code": "() => {\n const { data, isFetching, hasMore, totalItems, fetchNextPage } = useInfiniteData();\n\n return (\n <VStack gap={8}>\n <Text size='sm' color='secondary'>\n Loaded {data.length} of {totalItems} rows {isFetching && '— loading...'}\n {!hasMore && ' — all loaded'}\n </Text>\n <Table\n className='h-500'\n data={data}\n columns={securityColumns}\n getRowId={row => row.id}\n virtualized='container'\n isLoading={isFetching}\n onEndReached={fetchNextPage}\n onEndReachedThreshold={200}\n />\n </VStack>\n );\n}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.38.2",
3
+ "version": "0.39.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",