@jetbrains/ring-ui 8.0.0-beta.3 → 8.0.0-beta.5

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.
Files changed (89) hide show
  1. package/components/alert/alert-actions.d.ts +4 -0
  2. package/components/alert/alert-actions.js +5 -0
  3. package/components/alert/alert-heading.d.ts +4 -0
  4. package/components/alert/alert-heading.js +6 -0
  5. package/components/alert/alert.css +63 -22
  6. package/components/alert/alert.d.ts +14 -1
  7. package/components/alert/alert.js +34 -19
  8. package/components/alert/container.css +1 -2
  9. package/components/alert-service/alert-service.d.ts +2 -1
  10. package/components/alert-service/alert-service.js +8 -4
  11. package/components/auth/auth-core.d.ts +5 -9
  12. package/components/auth/auth-core.js +56 -15
  13. package/components/button-group/button-group.css +5 -5
  14. package/components/checkbox/checkbox.d.ts +1 -1
  15. package/components/collapsible-group/collapsible-group.css +138 -0
  16. package/components/collapsible-group/collapsible-group.d.ts +20 -0
  17. package/components/collapsible-group/collapsible-group.js +73 -0
  18. package/components/date-picker/consts.d.ts +1 -0
  19. package/components/date-picker/date-input.js +6 -2
  20. package/components/editable-heading/editable-heading.js +14 -14
  21. package/components/expand/collapsible-group.css +23 -89
  22. package/components/expand/collapsible-group.d.ts +8 -20
  23. package/components/expand/collapsible-group.js +14 -73
  24. package/components/global/compose-refs.d.ts +2 -1
  25. package/components/global/compose-refs.js +24 -8
  26. package/components/global/focus-with-temporary-tabindex.d.ts +11 -0
  27. package/components/global/focus-with-temporary-tabindex.js +21 -0
  28. package/components/global/intersection-observer-context.d.ts +29 -9
  29. package/components/global/intersection-observer-context.js +43 -24
  30. package/components/global/is-within-interactive-element.d.ts +6 -0
  31. package/components/global/is-within-interactive-element.js +26 -0
  32. package/components/global/is-within-navigable-element.d.ts +6 -0
  33. package/components/global/is-within-navigable-element.js +27 -0
  34. package/components/global/parse-css-duration.d.ts +5 -0
  35. package/components/global/parse-css-duration.js +13 -0
  36. package/components/global/schedule-with-cleanup.d.ts +12 -0
  37. package/components/global/schedule-with-cleanup.js +34 -0
  38. package/components/global/table-selection.js +1 -1
  39. package/components/header/header.css +3 -3
  40. package/components/http/http.d.ts +2 -2
  41. package/components/http/http.js +2 -2
  42. package/components/i18n/i18n.d.ts +1 -0
  43. package/components/i18n/messages.json +1 -0
  44. package/components/input/input.d.ts +1 -1
  45. package/components/legacy-table/row.d.ts +1 -1
  46. package/components/popup/popup.d.ts +2 -0
  47. package/components/popup/popup.js +2 -2
  48. package/components/select/select-popup.d.ts +1 -1
  49. package/components/select/select.d.ts +1 -1
  50. package/components/table/default-item-renderer.d.ts +29 -10
  51. package/components/table/default-item-renderer.js +53 -19
  52. package/components/table/internal/reorder-animation-context.d.ts +21 -0
  53. package/components/table/internal/reorder-animation-context.js +60 -0
  54. package/components/table/internal/reorder-handle.d.ts +9 -0
  55. package/components/table/internal/reorder-handle.js +334 -0
  56. package/components/table/internal/reorder-layout-context.d.ts +16 -0
  57. package/components/table/internal/reorder-layout-context.js +69 -0
  58. package/components/table/internal/table-header.d.ts +1 -0
  59. package/components/table/internal/table-header.js +143 -0
  60. package/components/table/internal/virtualization.d.ts +41 -0
  61. package/components/table/{table-virtualize.js → internal/virtualization.js} +74 -37
  62. package/components/table/item-virtualization.d.ts +37 -0
  63. package/components/table/item-virtualization.js +26 -0
  64. package/components/table/reorder-animation.d.ts +37 -0
  65. package/components/table/reorder-animation.js +11 -0
  66. package/components/table/reorder-item-layout.d.ts +32 -0
  67. package/components/table/reorder-item-layout.js +23 -0
  68. package/components/table/table-const.d.ts +8 -6
  69. package/components/table/table-const.js +7 -5
  70. package/components/table/table-primitives.d.ts +52 -16
  71. package/components/table/table-primitives.js +15 -60
  72. package/components/table/table-props.d.ts +304 -0
  73. package/components/table/table-props.js +1 -0
  74. package/components/table/table.css +179 -67
  75. package/components/table/table.d.ts +231 -209
  76. package/components/table/table.js +327 -2
  77. package/components/user-card/smart-user-card-tooltip.d.ts +1 -1
  78. package/components/util-stories.d.ts +26 -0
  79. package/components/util-stories.js +61 -0
  80. package/package.json +35 -36
  81. package/components/date-picker/use-intersection-observer.d.ts +0 -6
  82. package/components/date-picker/use-intersection-observer.js +0 -48
  83. package/components/global/composeRefs.d.ts +0 -6
  84. package/components/global/composeRefs.js +0 -7
  85. package/components/table/table-component.d.ts +0 -80
  86. package/components/table/table-component.js +0 -130
  87. package/components/table/table-row-focus.d.ts +0 -4
  88. package/components/table/table-row-focus.js +0 -42
  89. package/components/table/table-virtualize.d.ts +0 -32
@@ -0,0 +1,143 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import { use, useCallback, useRef, useState } from 'react';
3
+ import classNames from 'classnames';
4
+ import arrowDownIcon from '@jetbrains/icons/arrow-12px-down';
5
+ import arrowUpIcon from '@jetbrains/icons/arrow-12px-up';
6
+ import settingsIcon from '@jetbrains/icons/settings-12px';
7
+ import trashIcon from '@jetbrains/icons/trash-12px';
8
+ import unsortedIcon from '@jetbrains/icons/unsorted-12px';
9
+ import { TablePropsContext } from '../table-const';
10
+ import Icon from '../../icon';
11
+ import { isWithinInteractiveElement } from '../../global/is-within-interactive-element';
12
+ import { ReorderHandle } from './reorder-handle';
13
+ import { ReorderAnimationContext } from './reorder-animation-context';
14
+ import { ReorderLayoutContext, useReorderLayoutContextValue } from './reorder-layout-context';
15
+ import { useReorderItemLayout } from '../reorder-item-layout';
16
+ import styles from '../table.css';
17
+ export function TableHeader() {
18
+ const { columns, noHeader, stickyHeader, columnEditing, onColumnEditingRequest, theadClassName, theadTrClassName } = use(TablePropsContext);
19
+ const [localColumnEditing, setLocalColumnEditing] = useState(false);
20
+ const effectiveColumnEditing = columnEditing ?? localColumnEditing;
21
+ const toggleColumnEditing = useCallback((source) => {
22
+ let newColumnEditing;
23
+ if (columnEditing == null) {
24
+ newColumnEditing = !localColumnEditing;
25
+ setLocalColumnEditing(newColumnEditing);
26
+ }
27
+ else {
28
+ newColumnEditing = !columnEditing;
29
+ }
30
+ onColumnEditingRequest?.(newColumnEditing, source);
31
+ }, [columnEditing, localColumnEditing, onColumnEditingRequest]);
32
+ const handleTheadClick = useCallback((e) => {
33
+ if (window.matchMedia('(hover: none)').matches && !isWithinInteractiveElement(e.target)) {
34
+ toggleColumnEditing('header');
35
+ }
36
+ }, [toggleColumnEditing]);
37
+ const handleEditColumnsButtonClick = useCallback(() => {
38
+ toggleColumnEditing('edit-button');
39
+ }, [toggleColumnEditing]);
40
+ const reorderContextValue = useReorderLayoutContextValue();
41
+ if (noHeader)
42
+ return null;
43
+ return (
44
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events
45
+ <thead className={classNames(theadClassName, effectiveColumnEditing && styles.theadColumnEditing, stickyHeader && styles.stickyHeader)} onClick={handleTheadClick}>
46
+ <tr className={theadTrClassName}>
47
+ <ReorderLayoutContext value={reorderContextValue}>
48
+ {columns.map((column, columnIndex) => (<TableHeaderCell key={column.key} columnIndex={columnIndex} columnEditing={effectiveColumnEditing} handleEditColumnsButtonClick={handleEditColumnsButtonClick}/>))}
49
+ </ReorderLayoutContext>
50
+ </tr>
51
+ </thead>);
52
+ }
53
+ function TableHeaderCell({ columnIndex, columnEditing, handleEditColumnsButtonClick, }) {
54
+ const ref = useRef(null);
55
+ const { columns, columnEditButton } = use(TablePropsContext);
56
+ const { key, name, renderHeader, sortOrder, deletable, canReorder, thClassName } = columns[columnIndex];
57
+ const { reorderAnimation } = use(ReorderAnimationContext);
58
+ const children = renderHeader ? renderHeader() : (name ?? String(key));
59
+ useReorderItemLayout({
60
+ index: columnIndex,
61
+ getBounds: () => {
62
+ const r = ref.current?.getBoundingClientRect();
63
+ return { start: r?.left ?? 0, end: r?.right ?? 0 };
64
+ },
65
+ });
66
+ return (<th ref={ref} className={classNames(styles.headerCell, reorderAnimation?.direction === 'columns' &&
67
+ reorderAnimation.index === columnIndex &&
68
+ reorderAnimation.className, thClassName)} scope='col' aria-sort={sortOrder}>
69
+ <div className={styles.headerCellInnerWrapper}>
70
+ <div className={styles.sortAndHeader}>
71
+ {canReorder && <ColumnReorderHandle columnIndex={columnIndex}/>}
72
+ {sortOrder ? (<SortButton columnIndex={columnIndex} aria-description={`Sort order: ${sortOrder}`}>
73
+ {children}
74
+ </SortButton>) : (children)}
75
+ {canReorder && <ColumnReorderHandleMirror />}
76
+ </div>
77
+
78
+ <div className={styles.rightButtons}>
79
+ {deletable && <DeleteColumnButton columnIndex={columnIndex}/>}
80
+ {columnIndex === columns.length - 1 && columnEditButton && (<EditColumnsButton columnEditing={columnEditing} onClick={handleEditColumnsButtonClick}/>)}
81
+ </div>
82
+ </div>
83
+ </th>);
84
+ }
85
+ function SortButton({ columnIndex, className, children, onClick, ...restProps }) {
86
+ const tableProps = use(TablePropsContext);
87
+ const column = tableProps?.columns[columnIndex];
88
+ const sortOrder = column?.sortOrder;
89
+ const glyph = sortOrder === 'none'
90
+ ? unsortedIcon
91
+ : sortOrder === 'ascending'
92
+ ? arrowUpIcon
93
+ : sortOrder === 'descending'
94
+ ? arrowDownIcon
95
+ : undefined;
96
+ const handleClick = useCallback((e) => {
97
+ onClick?.(e);
98
+ if (!e.defaultPrevented) {
99
+ const nextOrder = sortOrder === 'ascending' ? 'descending' : sortOrder === 'descending' ? 'none' : 'ascending';
100
+ tableProps.onSort?.(columnIndex, nextOrder, tableProps.columns);
101
+ }
102
+ }, [columnIndex, onClick, sortOrder, tableProps]);
103
+ if (!tableProps || !column) {
104
+ return null;
105
+ }
106
+ return (<button type='button' className={classNames(styles.tableButton, className)} onClick={handleClick} {...restProps}>
107
+ {children} <Icon glyph={glyph} aria-hidden/>
108
+ </button>);
109
+ }
110
+ function DeleteColumnButton({ columnIndex, className, onClick, ...restProps }) {
111
+ const tableProps = use(TablePropsContext);
112
+ const column = tableProps?.columns[columnIndex];
113
+ const handleClick = useCallback((e) => {
114
+ onClick?.(e);
115
+ if (!e.defaultPrevented) {
116
+ const columns = tableProps.columns;
117
+ tableProps.onColumnDelete?.(columns[columnIndex], columnIndex, columns);
118
+ }
119
+ }, [columnIndex, onClick, tableProps]);
120
+ if (!tableProps || !column) {
121
+ return null;
122
+ }
123
+ const hint = `Delete column ${column.name ?? String(column.key)}.`;
124
+ return (<button type='button' className={classNames(styles.tableButton, styles.deleteColumnButton, className)} onClick={handleClick} aria-label={hint} title={hint} {...restProps}>
125
+ <Icon glyph={trashIcon} aria-hidden='true'/>
126
+ </button>);
127
+ }
128
+ function EditColumnsButton({ columnEditing, ...props }) {
129
+ const { className, ...restProps } = props;
130
+ const hint = columnEditing ? 'Hide column controls.' : 'Show column controls.';
131
+ return (<button type='button' className={classNames(styles.tableButton, styles.editColumnsButton, className)} aria-pressed={columnEditing} aria-label={hint} title={hint} {...restProps}>
132
+ <Icon glyph={settingsIcon} aria-hidden='true'/>
133
+ </button>);
134
+ }
135
+ function ColumnReorderHandle({ columnIndex, ...restProps }) {
136
+ return <ReorderHandle direction='columns' index={columnIndex} {...restProps}/>;
137
+ }
138
+ /**
139
+ * Reserves the space for the reorder handle and prevents layout shift when the handle appears on hover.
140
+ */
141
+ function ColumnReorderHandleMirror() {
142
+ return <span className={styles.columnReorderHandleMirror} aria-hidden='true'/>;
143
+ }
@@ -0,0 +1,41 @@
1
+ import { type RefObject } from 'react';
2
+ import { type IntersectionObserverHandle } from '../../global/intersection-observer-context';
3
+ export type VirtualItem = MaterializedItem | Spacer;
4
+ interface MaterializedItem {
5
+ type: 'materialized';
6
+ index: number;
7
+ }
8
+ interface Spacer {
9
+ type: 'spacer';
10
+ from: number;
11
+ to: number;
12
+ height: number;
13
+ key: string;
14
+ }
15
+ type CollapseItemIntoSpacerCallback = (index: number, height: number) => void;
16
+ interface VirtualizationContextValue {
17
+ intersectionObserverHandle: IntersectionObserverHandle;
18
+ collapseItemIntoSpacer: CollapseItemIntoSpacerCallback;
19
+ }
20
+ export declare const VirtualizationContext: import("react").Context<VirtualizationContextValue>;
21
+ export declare function useVirtualItems<T>({ enabled, data, data: { length }, scrollerRef, tableRef, estimateHeight, lookaheadPx, retentionMarginPx, minScrollAndResizeDeltaPx, }: {
22
+ enabled: boolean;
23
+ data: readonly T[];
24
+ scrollerRef: RefObject<HTMLElement | null> | undefined;
25
+ tableRef: RefObject<HTMLTableElement | null>;
26
+ estimateHeight: (item: T, index: number, items: readonly T[]) => number;
27
+ lookaheadPx: number;
28
+ retentionMarginPx: number;
29
+ minScrollAndResizeDeltaPx: number;
30
+ }): {
31
+ virtualItems: VirtualItem[];
32
+ virtualizationContextValue: {
33
+ intersectionObserverHandle: IntersectionObserverHandle;
34
+ collapseItemIntoSpacer: (index: number, height: number) => void;
35
+ };
36
+ };
37
+ export declare function SpacerRow({ spacer: { from, to, height }, colSpan }: {
38
+ spacer: Spacer;
39
+ colSpan: number;
40
+ }): import("react").JSX.Element;
41
+ export {};
@@ -1,52 +1,48 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { useIntersectionObserverHandle } from '../global/intersection-observer-context';
3
- import styles from './table.css';
1
+ import { createContext, useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useIntersectionObserverHandle, } from '../../global/intersection-observer-context';
3
+ import { setTimeoutWithCleanup } from '../../global/schedule-with-cleanup';
4
+ import styles from '../table.css';
5
+ export const VirtualizationContext = createContext({
6
+ intersectionObserverHandle: { observe: () => () => { } },
7
+ collapseItemIntoSpacer: () => { },
8
+ });
4
9
  /**
5
10
  * RAF is somewhat too frequent. Most updates happen on virtualization boundaries
6
11
  * within the invisible overscroll area, so they don't require such a high update rate.
7
12
  * Therefore, we throttle with a custom delay.
8
13
  */
9
14
  const virtualizationThrottleDelay = 50;
10
- export function useTableVirtualize({ enabled, data, data: { length }, scrollerRef, tableRef, estimateHeight, lookaheadPx, retentionMarginPx, minScrollAndResizeDeltaPx, }) {
15
+ export function useVirtualItems({ enabled, data, data: { length }, scrollerRef, tableRef, estimateHeight, lookaheadPx, retentionMarginPx, minScrollAndResizeDeltaPx, }) {
11
16
  const itemsMaterialization = useRef([]);
12
- const [virtualItems, setVirtualItems] = useState(() => [
13
- {
14
- type: 'spacer',
15
- from: 0,
16
- to: length,
17
- height: data.reduce((acc, item, index, items) => acc + estimateHeight(item, index, items), 0),
18
- key: `${styles.spacerRow}-0`,
19
- },
20
- ]);
17
+ const [virtualItems, setVirtualItems] = useState(() => enabled ? getAllVirtualizedItems(data, estimateHeight) : []);
21
18
  const materializeVisibleSpacerItems = useCallback(() => {
22
19
  if (!tableRef.current)
23
20
  return;
24
- const containerHeight = scrollerRef?.current?.clientHeight ?? window.innerHeight;
21
+ const scrollerRect = scrollerRef?.current?.getBoundingClientRect();
22
+ const visibleStart = scrollerRect?.top ?? 0;
23
+ const visibleEnd = scrollerRect ? Math.min(scrollerRect.bottom, window.innerHeight) : window.innerHeight;
24
+ const materializeStart = visibleStart - lookaheadPx;
25
+ const materializeEnd = visibleEnd + lookaheadPx;
25
26
  for (const spacerRow of tableRef.current.querySelectorAll(`.${styles.spacerRow}`)) {
26
- const rect = spacerRow.getBoundingClientRect();
27
- const spacerIntersectsLookaheadArea = rect.top < containerHeight + lookaheadPx && rect.bottom > -lookaheadPx;
28
- if (!spacerIntersectsLookaheadArea) {
27
+ const spacerRect = spacerRow.getBoundingClientRect();
28
+ if (spacerRect.bottom < materializeStart || spacerRect.top > materializeEnd) {
29
29
  continue;
30
30
  }
31
- const visibleOffsetStart = Math.max(0, -rect.top);
32
- const visibleOffsetEnd = Math.min(rect.height, containerHeight - rect.top);
33
- const materializeOffsetStart = visibleOffsetStart - lookaheadPx;
34
- const materializeOffsetEnd = visibleOffsetEnd + lookaheadPx;
35
- let offsetInSpacer = 0;
36
31
  const from = Number(spacerRow.dataset.from);
37
32
  const to = Number(spacerRow.dataset.to);
33
+ let currentTop = spacerRect.top;
38
34
  for (let i = from; i < to; i++) {
39
35
  const itemMaterialization = itemsMaterialization.current[i];
40
36
  const itemHeight = typeof itemMaterialization === 'number' ? itemMaterialization : estimateHeight(data[i], i, data);
41
- const itemOffsetStart = offsetInSpacer;
42
- const itemOffsetEnd = offsetInSpacer + itemHeight;
43
- if (itemOffsetStart < materializeOffsetEnd && itemOffsetEnd > materializeOffsetStart) {
37
+ const itemTop = currentTop;
38
+ const itemBottom = currentTop + itemHeight;
39
+ if (itemBottom >= materializeStart && itemTop <= materializeEnd) {
44
40
  itemsMaterialization.current[i] = true;
45
41
  }
46
- else if (itemOffsetStart > materializeOffsetEnd) {
42
+ else if (itemTop > materializeEnd) {
47
43
  break;
48
44
  }
49
- offsetInSpacer += itemHeight;
45
+ currentTop += itemHeight;
50
46
  }
51
47
  }
52
48
  }, [data, estimateHeight, lookaheadPx, scrollerRef, tableRef]);
@@ -56,7 +52,7 @@ export function useTableVirtualize({ enabled, data, data: { length }, scrollerRe
56
52
  for (let i = 0; i < length; i++) {
57
53
  const itemMaterialization = itemsMaterialization.current[i];
58
54
  if (itemMaterialization === true) {
59
- newVirtualItems.push({ type: 'rendered', index: i });
55
+ newVirtualItems.push({ type: 'materialized', index: i });
60
56
  }
61
57
  else {
62
58
  const lastItemOrSpacer = newVirtualItems[newVirtualItems.length - 1];
@@ -72,7 +68,7 @@ export function useTableVirtualize({ enabled, data, data: { length }, scrollerRe
72
68
  from: i,
73
69
  to: i + 1,
74
70
  height,
75
- key: `${styles.spacerRow}-${spacerCounter++}`,
71
+ key: spacerKey(spacerCounter++),
76
72
  });
77
73
  }
78
74
  }
@@ -96,7 +92,10 @@ export function useTableVirtualize({ enabled, data, data: { length }, scrollerRe
96
92
  callbacksRef.current = null;
97
93
  }, virtualizationThrottleDelay);
98
94
  }, []);
95
+ const prevEnabledRef = useRef(enabled);
99
96
  useEffect(() => {
97
+ const prevEnabled = prevEnabledRef.current;
98
+ prevEnabledRef.current = enabled;
100
99
  if (!enabled)
101
100
  return;
102
101
  const scroller = scrollerRef?.current;
@@ -121,27 +120,65 @@ export function useTableVirtualize({ enabled, data, data: { length }, scrollerRe
121
120
  const resizeObserver = new ResizeObserver(handleViewportChange);
122
121
  const resizeTarget = scroller ?? document.documentElement;
123
122
  resizeObserver.observe(resizeTarget);
124
- handleViewportChange();
123
+ let timeoutCleanup;
124
+ if (!prevEnabled) {
125
+ timeoutCleanup = setTimeoutWithCleanup(() => {
126
+ itemsMaterialization.current = [];
127
+ setVirtualItems(getAllVirtualizedItems(data, estimateHeight));
128
+ handleViewportChange();
129
+ });
130
+ }
131
+ else {
132
+ handleViewportChange();
133
+ }
125
134
  return () => {
135
+ timeoutCleanup?.();
126
136
  scrollTarget.removeEventListener('scroll', handleViewportChange);
127
137
  resizeObserver.unobserve(resizeTarget);
128
138
  resizeObserver.disconnect();
139
+ if (timerIdRef.current != null) {
140
+ window.clearTimeout(timerIdRef.current);
141
+ timerIdRef.current = null;
142
+ callbacksRef.current = null;
143
+ }
129
144
  };
130
- }, [enabled, materializeVisibleSpacerItems, minScrollAndResizeDeltaPx, recomputeVirtualItems, scrollerRef, throttle]);
145
+ }, [
146
+ data,
147
+ enabled,
148
+ estimateHeight,
149
+ length,
150
+ materializeVisibleSpacerItems,
151
+ minScrollAndResizeDeltaPx,
152
+ recomputeVirtualItems,
153
+ scrollerRef,
154
+ throttle,
155
+ ]);
131
156
  const intersectionObserverHandle = useIntersectionObserverHandle(scrollerRef, scrollerRef ? retentionMarginPx : undefined, !scrollerRef ? retentionMarginPx : undefined);
132
- const collapseItemIntoSpacer = useCallback((index, height) => {
157
+ function collapseItemIntoSpacer(index, height) {
133
158
  if (!enabled)
134
159
  return;
135
160
  itemsMaterialization.current[index] = height;
136
161
  throttle(recomputeVirtualItems);
137
- }, [enabled, throttle, recomputeVirtualItems]);
138
- const allVisibleVirtualItems = useMemo(() => Array.from({ length: enabled ? 0 : length }, (_, index) => ({ type: 'rendered', index })), [enabled, length]);
162
+ }
139
163
  return {
140
- virtualItems: enabled ? virtualItems : allVisibleVirtualItems,
141
- intersectionObserverHandle,
142
- collapseItemIntoSpacer,
164
+ virtualItems,
165
+ virtualizationContextValue: { intersectionObserverHandle, collapseItemIntoSpacer },
143
166
  };
144
167
  }
168
+ function getAllVirtualizedItems(data, estimateHeight) {
169
+ return [
170
+ {
171
+ type: 'spacer',
172
+ from: 0,
173
+ to: data.length,
174
+ height: data.reduce((acc, item, index, items) => acc + estimateHeight(item, index, items), 0),
175
+ key: spacerKey(0),
176
+ },
177
+ ];
178
+ }
179
+ function spacerKey(index) {
180
+ return `__${styles.spacerRow}-${index}`;
181
+ }
145
182
  export function SpacerRow({ spacer: { from, to, height }, colSpan }) {
146
183
  return (<tr className={styles.spacerRow} data-from={from} data-to={to}>
147
184
  <td className={styles.spacerCell} colSpan={colSpan} style={{ height }}/>
@@ -0,0 +1,37 @@
1
+ import { type RefObject } from 'react';
2
+ /**
3
+ * Use in an item renderer to control item virtualization.
4
+ */
5
+ export declare function useItemVirtualization({ index, refs, onIntersectionChange, }: {
6
+ /**
7
+ * Index of the item.
8
+ */
9
+ index: number;
10
+ /**
11
+ * One or more elements representing this item.
12
+ *
13
+ * When multiple elements are provided, the virtualization callback receives
14
+ * the intersection state of all of them.
15
+ *
16
+ * If you pass multiple refs, pass a stable array reference to avoid restarting
17
+ * observation on every render — for example, memoize it with `useMemo()`,
18
+ * unless you use the React Compiler.
19
+ */
20
+ refs: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[];
21
+ /**
22
+ * Invoked when the `isIntersecting` state of the observed elements changes.
23
+ * Pass a stable function reference to avoid restarting observation on every render —
24
+ * for example, wrap it with `useCallback()`, unless you use the React Compiler.
25
+ *
26
+ * @param intersectionStates - Current intersection state of every observed element.
27
+ * Entries are initially `undefined` until the corresponding element
28
+ * has been reported by `IntersectionObserver`.
29
+ * @param changedIndex - Index of the element whose intersection state changed.
30
+ * @param elements - The observed elements. Note that some elements may
31
+ * already be disconnected from the DOM when this callback is invoked.
32
+ * Use additional checks like `element.isConnected`.
33
+ * @returns Return the height of an item to collapse the item into spacer,
34
+ * or `undefined` to keep the item rendered.
35
+ */
36
+ onIntersectionChange: (intersectionStates: (boolean | undefined)[], changedIndex: number, elements: (Element | null)[]) => number | undefined;
37
+ }): void;
@@ -0,0 +1,26 @@
1
+ import { use, useEffect } from 'react';
2
+ import { VirtualizationContext } from './internal/virtualization';
3
+ /**
4
+ * Use in an item renderer to control item virtualization.
5
+ */
6
+ export function useItemVirtualization({ index, refs, onIntersectionChange, }) {
7
+ const { intersectionObserverHandle, collapseItemIntoSpacer } = use(VirtualizationContext);
8
+ useEffect(() => {
9
+ const intersectionStates = Array.isArray(refs) ? refs.map(() => undefined) : [undefined];
10
+ const elements = Array.isArray(refs) ? refs.map(r => r.current) : [refs.current];
11
+ const cleanups = [];
12
+ elements.forEach((element, elementIndex) => {
13
+ if (!element)
14
+ return;
15
+ const cleanup = intersectionObserverHandle.observe(element, isIntersecting => {
16
+ intersectionStates[elementIndex] = isIntersecting;
17
+ const height = onIntersectionChange(intersectionStates, elementIndex, elements);
18
+ if (height != null) {
19
+ collapseItemIntoSpacer(index, height);
20
+ }
21
+ });
22
+ cleanups.push(cleanup);
23
+ });
24
+ return () => cleanups.forEach(cleanup => cleanup());
25
+ }, [collapseItemIntoSpacer, intersectionObserverHandle, index, onIntersectionChange, refs]);
26
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Information about a column or item currently being animated after reorder.
3
+ *
4
+ * Available through {@link useReorderAnimation} to allow custom cell
5
+ * renderers to animate reordered columns and items consistently with the default
6
+ * table renderer.
7
+ */
8
+ export interface ReorderAnimation {
9
+ /**
10
+ * What is animated: columns or items (rows).
11
+ */
12
+ direction: 'columns' | 'items';
13
+ /**
14
+ * Index of the column or item being animated.
15
+ */
16
+ index: number;
17
+ /**
18
+ * Current animation phase.
19
+ */
20
+ phase: 'initial' | 'fade-out';
21
+ /**
22
+ * CSS class to apply to the animated row, cell, or another element used to
23
+ * render the animation on this phase.
24
+ *
25
+ * The class sets `background-color` and `transition`. If your custom styles
26
+ * also define `transition` on the same element, they may conflict with the
27
+ * animation.
28
+ */
29
+ className: string;
30
+ }
31
+ /**
32
+ * Provides information about the currently animated column or item after reorder.
33
+ *
34
+ * Use in a custom cell renderer to animate reordered columns and items
35
+ * consistently with the default table renderer.
36
+ */
37
+ export declare function useReorderAnimation(): ReorderAnimation | null;
@@ -0,0 +1,11 @@
1
+ import { use } from 'react';
2
+ import { ReorderAnimationContext } from './internal/reorder-animation-context';
3
+ /**
4
+ * Provides information about the currently animated column or item after reorder.
5
+ *
6
+ * Use in a custom cell renderer to animate reordered columns and items
7
+ * consistently with the default table renderer.
8
+ */
9
+ export function useReorderAnimation() {
10
+ return use(ReorderAnimationContext).reorderAnimation;
11
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Registers the physical boundaries of a custom-rendered item with the reorder
3
+ * system, so that the insertion indicator and insertion point calculation are
4
+ * correct for items that span multiple rows or have non-standard sizing.
5
+ *
6
+ * `DefaultItemRenderer` calls this automatically. Use this hook in a custom item
7
+ * renderer when the default boundary measurement is insufficient — for example,
8
+ * when the item spans multiple rows, or when you don't use
9
+ * the `DefaultItemRenderer` at all.
10
+ *
11
+ * If you include `DefaultItemRenderer` inside your custom renderer, set its
12
+ * `noReorderLayout` prop to `true` to prevent double registration.
13
+ */
14
+ export declare function useReorderItemLayout({ disabled, index, getBounds, }: {
15
+ /**
16
+ * When `true`, the item is not registered. Use to disable without violating
17
+ * the rules of hooks.
18
+ */
19
+ disabled?: boolean;
20
+ /**
21
+ * Index of the item in the `data` array.
22
+ */
23
+ index: number;
24
+ /**
25
+ * Returns the item's top and bottom client coordinates (in pixels, relative
26
+ * to the viewport).
27
+ */
28
+ getBounds: () => {
29
+ start: number;
30
+ end: number;
31
+ };
32
+ }): void;
@@ -0,0 +1,23 @@
1
+ import { use, useEffect } from 'react';
2
+ import { ReorderLayoutContext } from './internal/reorder-layout-context';
3
+ /**
4
+ * Registers the physical boundaries of a custom-rendered item with the reorder
5
+ * system, so that the insertion indicator and insertion point calculation are
6
+ * correct for items that span multiple rows or have non-standard sizing.
7
+ *
8
+ * `DefaultItemRenderer` calls this automatically. Use this hook in a custom item
9
+ * renderer when the default boundary measurement is insufficient — for example,
10
+ * when the item spans multiple rows, or when you don't use
11
+ * the `DefaultItemRenderer` at all.
12
+ *
13
+ * If you include `DefaultItemRenderer` inside your custom renderer, set its
14
+ * `noReorderLayout` prop to `true` to prevent double registration.
15
+ */
16
+ export function useReorderItemLayout({ disabled, index, getBounds, }) {
17
+ const { registerReorderItem } = use(ReorderLayoutContext);
18
+ useEffect(() => {
19
+ if (disabled)
20
+ return;
21
+ return registerReorderItem(index, getBounds);
22
+ }, [index, getBounds, registerReorderItem, disabled]);
23
+ }
@@ -1,8 +1,10 @@
1
- import type { TableProps } from './table';
1
+ import type { TableProps } from './table-props';
2
+ /**
3
+ * Use anywhere inside the table to get access to the props passed to it.
4
+ * Cast to `Context<TableProps<T>>` in usage place.
5
+ */
2
6
  export declare const TablePropsContext: import("react").Context<TableProps<unknown> | null>;
3
- export declare const ColumnIndexContext: import("react").Context<number>;
4
- export declare const CollapseItemIntoSpacerContext: import("react").Context<(height: number) => void>;
7
+ /**
8
+ * When a row only contains unformatted single-line text, it will be exactly of this height.
9
+ */
5
10
  export declare const defaultRowHeight = 37;
6
- export declare const defaultLookaheadPx = 400;
7
- export declare const defaultRetentionMarginPx = 450;
8
- export declare const defaultMinScrollAndResizeDeltaPx = 50;
@@ -1,8 +1,10 @@
1
1
  import { createContext } from 'react';
2
+ /**
3
+ * Use anywhere inside the table to get access to the props passed to it.
4
+ * Cast to `Context<TableProps<T>>` in usage place.
5
+ */
2
6
  export const TablePropsContext = createContext(null);
3
- export const ColumnIndexContext = createContext(-1);
4
- export const CollapseItemIntoSpacerContext = createContext(() => { });
7
+ /**
8
+ * When a row only contains unformatted single-line text, it will be exactly of this height.
9
+ */
5
10
  export const defaultRowHeight = 37;
6
- export const defaultLookaheadPx = 400;
7
- export const defaultRetentionMarginPx = 450;
8
- export const defaultMinScrollAndResizeDeltaPx = 50;
@@ -1,15 +1,4 @@
1
- import { type ComponentPropsWithRef } from 'react';
2
- /**
3
- * Include it in a column header to make the column sortable.
4
- * Handle clicks with {@link TableProps.onSort}.
5
- */
6
- export declare function SortButton<T>({ className, children, onClick, ...restProps }: ComponentPropsWithRef<'button'>): import("react").JSX.Element | null;
7
- /**
8
- * Include it in a column header to make the column deletable.
9
- * Beware that `column.name ?? String(column.key)` is used in the aria-label.
10
- * Handle clicks with {@link TableProps.onColumnDelete}.
11
- */
12
- export declare function DeleteColumnButton<T>({ className, onClick, ...restProps }: ComponentPropsWithRef<'button'>): import("react").JSX.Element | null;
1
+ import type { ComponentPropsWithRef } from 'react';
13
2
  export interface TableRowProps {
14
3
  /**
15
4
  * @see DefaultItemRendererProps.keyboardFocusable
@@ -17,12 +6,59 @@ export interface TableRowProps {
17
6
  keyboardFocusable?: boolean;
18
7
  }
19
8
  /**
20
- * A helper `<tr>` component for a custom {@link TableProps.renderItem} implementations.
21
- * Applies the standard row classnames.
9
+ * @internal
10
+ */
11
+ export declare const keyboardFocusableAttrName = "data-keyboard-focusable";
12
+ /**
13
+ * A helper `<tr>` component for custom {@link TableProps.renderItem} implementations.
14
+ * Applies the standard row class names.
22
15
  */
23
16
  export declare function TableRow(props: TableRowProps & ComponentPropsWithRef<'tr'>): import("react").JSX.Element;
24
17
  /**
25
- * A helper `<td>` component for a custom {@link TableProps.renderItem} implementations.
26
- * Applies the standard cell classnames, but not data-dependent `tdClassName`.
18
+ * A helper `<td>` component for custom {@link TableProps.renderItem} implementations.
19
+ * Applies the standard cell class names, but not data-dependent `tdClassName`.
27
20
  */
28
21
  export declare function TableCell(props: ComponentPropsWithRef<'td'>): import("react").JSX.Element;
22
+ /**
23
+ * - `'pointerdown'` means the user has pressed the mouse button or touched the handle.
24
+ * - `number` means the distance in pixels the user has dragged the handle along
25
+ * the Y axis.
26
+ * - `'cancelled'` means the reorder was aborted — e.g. the user pressed Escape,
27
+ * released the pointer outside the browser, or dropped the item at its original
28
+ * position. Use this phase to play a cancellation animation in your custom renderer.
29
+ * This state lasts 600 ms (matching the built-in cancel animation), after which
30
+ * `undefined` is sent.
31
+ * - `undefined` means the drag interaction is fully over. Sent after a successful
32
+ * reorder as well as after the `'cancelled'` phase, so you can always do cleanup
33
+ * here without managing your own timer.
34
+ */
35
+ export type DragState = 'pointerdown' | number | 'cancelled' | undefined;
36
+ export interface ItemReorderHandleProps {
37
+ /**
38
+ * The index of the item in the table data array.
39
+ */
40
+ index: number;
41
+ /**
42
+ * When `true`, the drag frame, indicating the currently dragged item,
43
+ * will not be rendered.
44
+ */
45
+ noDragFrame?: boolean;
46
+ /**
47
+ * When `true`, the drag handle stays fixed in place instead of following
48
+ * the pointer during drag.
49
+ */
50
+ noHandleTranslate?: boolean;
51
+ /**
52
+ * Callback that is called when the user drags the handle.
53
+ * Use it for custom drag indication.
54
+ * The `TableProps.onItemReorder` will be called after this callback.
55
+ * @param state The current drag state.
56
+ */
57
+ onUserDrag?: (state: DragState) => void;
58
+ }
59
+ /**
60
+ * A drag handle that allows the user to reorder the row. Place it anywhere
61
+ * inside a row renderer.
62
+ * Use {@link TableProps.canReorderItem} and {@link TableProps.onItemReorder}.
63
+ */
64
+ export declare function ItemReorderHandle({ index, ...restProps }: ItemReorderHandleProps & ComponentPropsWithRef<'button'>): import("react").JSX.Element;