@jetbrains/ring-ui 8.0.0-beta.4 → 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 (51) 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/date-picker/consts.d.ts +1 -0
  15. package/components/date-picker/date-input.js +6 -2
  16. package/components/header/header.css +3 -3
  17. package/components/http/http.d.ts +2 -2
  18. package/components/http/http.js +2 -2
  19. package/components/i18n/i18n.d.ts +1 -0
  20. package/components/i18n/messages.json +1 -0
  21. package/components/table/default-item-renderer.d.ts +7 -1
  22. package/components/table/default-item-renderer.js +18 -5
  23. package/components/table/internal/reorder-animation-context.d.ts +21 -0
  24. package/components/table/internal/reorder-animation-context.js +60 -0
  25. package/components/table/internal/reorder-handle.d.ts +9 -0
  26. package/components/table/internal/reorder-handle.js +334 -0
  27. package/components/table/internal/reorder-layout-context.d.ts +16 -0
  28. package/components/table/internal/reorder-layout-context.js +69 -0
  29. package/components/table/internal/table-header.d.ts +1 -4
  30. package/components/table/internal/table-header.js +32 -246
  31. package/components/table/internal/{virtual-items.d.ts → virtualization.d.ts} +10 -3
  32. package/components/table/internal/{virtual-items.js → virtualization.js} +8 -6
  33. package/components/table/item-virtualization.d.ts +5 -3
  34. package/components/table/item-virtualization.js +4 -6
  35. package/components/table/reorder-animation.d.ts +37 -0
  36. package/components/table/reorder-animation.js +11 -0
  37. package/components/table/reorder-item-layout.d.ts +32 -0
  38. package/components/table/reorder-item-layout.js +23 -0
  39. package/components/table/table-const.d.ts +0 -32
  40. package/components/table/table-const.js +0 -7
  41. package/components/table/table-primitives.d.ts +43 -0
  42. package/components/table/table-primitives.js +9 -0
  43. package/components/table/table-props.d.ts +29 -5
  44. package/components/table/table.css +17 -10
  45. package/components/table/table.d.ts +31 -5
  46. package/components/table/table.js +64 -35
  47. package/components/user-card/smart-user-card-tooltip.d.ts +1 -1
  48. package/components/util-stories.d.ts +2 -2
  49. package/package.json +12 -12
  50. package/components/table/internal/column-animation.d.ts +0 -16
  51. package/components/table/internal/column-animation.js +0 -45
@@ -1,4 +1 @@
1
- import { type ExpectColumnReorder } from './column-animation';
2
- export declare function TableHeader<T>({ expectColumnReorder }: {
3
- expectColumnReorder: ExpectColumnReorder;
4
- }): import("react").JSX.Element | null;
1
+ export declare function TableHeader<T>(): import("react").JSX.Element | null;
@@ -1,19 +1,20 @@
1
- /* eslint-disable no-nested-ternary, max-lines */
1
+ /* eslint-disable no-nested-ternary */
2
2
  import { use, useCallback, useRef, useState } from 'react';
3
3
  import classNames from 'classnames';
4
4
  import arrowDownIcon from '@jetbrains/icons/arrow-12px-down';
5
5
  import arrowUpIcon from '@jetbrains/icons/arrow-12px-up';
6
- import dragIcon from '@jetbrains/icons/drag-12px';
7
6
  import settingsIcon from '@jetbrains/icons/settings-12px';
8
7
  import trashIcon from '@jetbrains/icons/trash-12px';
9
8
  import unsortedIcon from '@jetbrains/icons/unsorted-12px';
10
- import { ColumnAnimationContext, TablePropsContext } from '../table-const';
9
+ import { TablePropsContext } from '../table-const';
11
10
  import Icon from '../../icon';
12
- import { useComposedRef } from '../../global/compose-refs';
13
11
  import { isWithinInteractiveElement } from '../../global/is-within-interactive-element';
14
- import { parseCssDuration } from '../../global/parse-css-duration';
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';
15
16
  import styles from '../table.css';
16
- export function TableHeader({ expectColumnReorder }) {
17
+ export function TableHeader() {
17
18
  const { columns, noHeader, stickyHeader, columnEditing, onColumnEditingRequest, theadClassName, theadTrClassName } = use(TablePropsContext);
18
19
  const [localColumnEditing, setLocalColumnEditing] = useState(false);
19
20
  const effectiveColumnEditing = columnEditing ?? localColumnEditing;
@@ -36,25 +37,38 @@ export function TableHeader({ expectColumnReorder }) {
36
37
  const handleEditColumnsButtonClick = useCallback(() => {
37
38
  toggleColumnEditing('edit-button');
38
39
  }, [toggleColumnEditing]);
40
+ const reorderContextValue = useReorderLayoutContextValue();
39
41
  if (noHeader)
40
42
  return null;
41
43
  return (
42
44
  // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events
43
45
  <thead className={classNames(theadClassName, effectiveColumnEditing && styles.theadColumnEditing, stickyHeader && styles.stickyHeader)} onClick={handleTheadClick}>
44
46
  <tr className={theadTrClassName}>
45
- {columns.map((column, columnIndex) => (<TableHeaderCell key={column.key} columnIndex={columnIndex} columnEditing={effectiveColumnEditing} handleEditColumnsButtonClick={handleEditColumnsButtonClick} expectColumnReorder={expectColumnReorder}/>))}
47
+ <ReorderLayoutContext value={reorderContextValue}>
48
+ {columns.map((column, columnIndex) => (<TableHeaderCell key={column.key} columnIndex={columnIndex} columnEditing={effectiveColumnEditing} handleEditColumnsButtonClick={handleEditColumnsButtonClick}/>))}
49
+ </ReorderLayoutContext>
46
50
  </tr>
47
51
  </thead>);
48
52
  }
49
- function TableHeaderCell({ columnIndex, columnEditing, handleEditColumnsButtonClick, expectColumnReorder, }) {
53
+ function TableHeaderCell({ columnIndex, columnEditing, handleEditColumnsButtonClick, }) {
54
+ const ref = useRef(null);
50
55
  const { columns, columnEditButton } = use(TablePropsContext);
51
56
  const { key, name, renderHeader, sortOrder, deletable, canReorder, thClassName } = columns[columnIndex];
52
- const animatedColumn = use(ColumnAnimationContext);
57
+ const { reorderAnimation } = use(ReorderAnimationContext);
53
58
  const children = renderHeader ? renderHeader() : (name ?? String(key));
54
- return (<th className={classNames(styles.headerCell, animatedColumn?.columnIndex === columnIndex && animatedColumn.cellClassName, thClassName)} scope='col' aria-sort={sortOrder}>
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}>
55
69
  <div className={styles.headerCellInnerWrapper}>
56
70
  <div className={styles.sortAndHeader}>
57
- {canReorder && <ColumnReorderHandle columnIndex={columnIndex} expectColumnReorder={expectColumnReorder}/>}
71
+ {canReorder && <ColumnReorderHandle columnIndex={columnIndex}/>}
58
72
  {sortOrder ? (<SortButton columnIndex={columnIndex} aria-description={`Sort order: ${sortOrder}`}>
59
73
  {children}
60
74
  </SortButton>) : (children)}
@@ -89,7 +103,7 @@ function SortButton({ columnIndex, className, children, onClick, ...restProps })
89
103
  if (!tableProps || !column) {
90
104
  return null;
91
105
  }
92
- return (<button type='button' className={classNames(styles.headerButton, className)} onClick={handleClick} {...restProps}>
106
+ return (<button type='button' className={classNames(styles.tableButton, className)} onClick={handleClick} {...restProps}>
93
107
  {children} <Icon glyph={glyph} aria-hidden/>
94
108
  </button>);
95
109
  }
@@ -99,255 +113,27 @@ function DeleteColumnButton({ columnIndex, className, onClick, ...restProps }) {
99
113
  const handleClick = useCallback((e) => {
100
114
  onClick?.(e);
101
115
  if (!e.defaultPrevented) {
102
- tableProps.onColumnDelete?.(columnIndex, tableProps.columns);
116
+ const columns = tableProps.columns;
117
+ tableProps.onColumnDelete?.(columns[columnIndex], columnIndex, columns);
103
118
  }
104
119
  }, [columnIndex, onClick, tableProps]);
105
120
  if (!tableProps || !column) {
106
121
  return null;
107
122
  }
108
123
  const hint = `Delete column ${column.name ?? String(column.key)}.`;
109
- return (<button type='button' className={classNames(styles.headerButton, styles.deleteColumnButton, className)} onClick={handleClick} aria-label={hint} title={hint} {...restProps}>
124
+ return (<button type='button' className={classNames(styles.tableButton, styles.deleteColumnButton, className)} onClick={handleClick} aria-label={hint} title={hint} {...restProps}>
110
125
  <Icon glyph={trashIcon} aria-hidden='true'/>
111
126
  </button>);
112
127
  }
113
128
  function EditColumnsButton({ columnEditing, ...props }) {
114
129
  const { className, ...restProps } = props;
115
130
  const hint = columnEditing ? 'Hide column controls.' : 'Show column controls.';
116
- return (<button type='button' className={classNames(styles.headerButton, styles.editColumnsButton, className)} aria-pressed={columnEditing} aria-label={hint} title={hint} {...restProps}>
131
+ return (<button type='button' className={classNames(styles.tableButton, styles.editColumnsButton, className)} aria-pressed={columnEditing} aria-label={hint} title={hint} {...restProps}>
117
132
  <Icon glyph={settingsIcon} aria-hidden='true'/>
118
133
  </button>);
119
134
  }
120
- function ColumnReorderHandle({ columnIndex, expectColumnReorder, ref: userRef, className, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture, onKeyDown, ...restProps }) {
121
- const tableProps = use(TablePropsContext);
122
- const column = tableProps?.columns[columnIndex];
123
- const canReorder = column?.canReorder;
124
- const canReorderCb = useCallback((index) => canReorder === true || (typeof canReorder === 'function' && canReorder(index, tableProps.columns)), [canReorder, tableProps]);
125
- const localRef = useRef(null);
126
- const composedRef = useComposedRef(localRef, userRef);
127
- const activeDragRef = useRef(null);
128
- const getDragFrame = useCallback(() => {
129
- return document.body.querySelector(`.${styles.columnDragFrame}`);
130
- }, []);
131
- const renderDragFrame = useCallback((clientX) => {
132
- const { startColumnIndex, startClientX, headerTopClientY, columnsClientX, indicatorHeight } = activeDragRef.current;
133
- const { l, r } = columnsClientX[startColumnIndex];
134
- let dragFrame = getDragFrame();
135
- if (!dragFrame) {
136
- dragFrame = document.createElement('div');
137
- dragFrame.className = styles.columnDragFrame;
138
- dragFrame.style.setProperty('top', `calc(max(0px, ${headerTopClientY - 2}px))`);
139
- dragFrame.style.setProperty('width', `${r - l}px`);
140
- dragFrame.style.setProperty('height', indicatorHeight);
141
- document.body.appendChild(dragFrame);
142
- }
143
- dragFrame.style.setProperty('left', `${l + clientX - startClientX}px`);
144
- }, [getDragFrame]);
145
- const translateXButton = useCallback((clientX) => {
146
- if (!localRef.current || !activeDragRef.current)
147
- return;
148
- const { startClientX } = activeDragRef.current;
149
- localRef.current.style.setProperty('transform', `translateX(${clientX - startClientX}px)`);
150
- }, []);
151
- const getClosestInsertionPoint = useCallback((clientX) => {
152
- let bestDistance = Infinity;
153
- let index = -1;
154
- let after = false;
155
- activeDragRef.current?.columnsClientX.forEach(({ l, r }, i) => {
156
- const distanceToLeft = Math.abs(l - clientX);
157
- const distanceToRight = Math.abs(r - clientX);
158
- if (distanceToLeft < bestDistance && canReorderCb(i)) {
159
- bestDistance = distanceToLeft;
160
- index = i;
161
- after = false;
162
- }
163
- if (distanceToRight < bestDistance && canReorderCb(i + 1)) {
164
- bestDistance = distanceToRight;
165
- index = i;
166
- after = true;
167
- }
168
- });
169
- return { index, after };
170
- }, [canReorderCb]);
171
- const getInsertionIndicator = useCallback(() => {
172
- return document.body.querySelector(`.${styles.columnInsertionIndicator}`);
173
- }, []);
174
- const renderInsertionIndicator = useCallback((insertionPoint) => {
175
- const { index, after } = insertionPoint;
176
- const { headerTopClientY, columnsClientX, indicatorHeight } = activeDragRef.current;
177
- const { l, r } = columnsClientX[index];
178
- let indicator = getInsertionIndicator();
179
- if (!indicator) {
180
- indicator = document.createElement('div');
181
- indicator.className = styles.columnInsertionIndicator;
182
- indicator.style.setProperty('top', `${headerTopClientY}px`);
183
- indicator.style.setProperty('height', indicatorHeight);
184
- document.body.appendChild(indicator);
185
- }
186
- indicator.style.setProperty('left', `${(after ? r : l) - 1}px`);
187
- }, [getInsertionIndicator]);
188
- const cleanupDrag = useCallback(() => {
189
- if (activeDragRef.current) {
190
- activeDragRef.current.cleanup();
191
- activeDragRef.current = null;
192
- }
193
- const btn = localRef.current;
194
- if (btn) {
195
- btn.style.removeProperty('transform');
196
- btn.style.removeProperty('transition');
197
- }
198
- getDragFrame()?.remove();
199
- getInsertionIndicator()?.remove();
200
- }, [getDragFrame, getInsertionIndicator]);
201
- const animateNoChangeThenCleanup = useCallback(() => {
202
- if (activeDragRef.current?.state === 'is-dragging') {
203
- activeDragRef.current.state = 'ended-with-no-change';
204
- activeDragRef.current.cleanup();
205
- activeDragRef.current.cleanup = () => { };
206
- const dragFrame = getDragFrame();
207
- if (dragFrame) {
208
- const { columnsClientX, startColumnIndex } = activeDragRef.current;
209
- dragFrame.style.left = `${columnsClientX[startColumnIndex].l}px`;
210
- dragFrame.style.opacity = '0';
211
- dragFrame.style.transition = 'left var(--ring-ease), opacity var(--ring-ease)';
212
- }
213
- const indicator = getInsertionIndicator();
214
- if (indicator) {
215
- indicator.style.opacity = '0';
216
- indicator.style.transition = 'opacity var(--ring-ease)';
217
- }
218
- const btn = localRef.current;
219
- if (btn) {
220
- btn.style.transform = 'translateX(0)';
221
- btn.style.transition = 'transform var(--ring-ease)';
222
- }
223
- }
224
- const ringEaseMs = parseCssDuration(window.getComputedStyle(document.documentElement).getPropertyValue('--ring-ease'));
225
- setTimeout(cleanupDrag, ringEaseMs);
226
- }, [cleanupDrag, getDragFrame, getInsertionIndicator]);
227
- const handlePointerDown = useCallback((e) => {
228
- onPointerDown?.(e);
229
- if (e.defaultPrevented)
230
- return;
231
- const { clientX: startClientX, pointerId, currentTarget } = e;
232
- const thead = currentTarget.closest('thead');
233
- const table = thead?.closest('table');
234
- if (!thead || !table)
235
- return;
236
- const headerTopClientY = thead.getBoundingClientRect().top;
237
- const columnsClientX = [...thead.querySelectorAll('th')].map(th => {
238
- const rect = th.getBoundingClientRect();
239
- return { l: rect.x, r: rect.x + rect.width };
240
- });
241
- const { bottom } = table.getBoundingClientRect();
242
- const visibleTableHeight = bottom - headerTopClientY;
243
- const viewportBottomRelativeToHeaderTop = window.innerHeight - headerTopClientY;
244
- const indicatorHeight = `min(${visibleTableHeight}px, calc(${viewportBottomRelativeToHeaderTop}px - .5rem))`;
245
- currentTarget.setPointerCapture(pointerId);
246
- function keydownListener(keyEvent) {
247
- if (keyEvent.key === 'Escape') {
248
- animateNoChangeThenCleanup();
249
- keyEvent.stopPropagation();
250
- keyEvent.preventDefault();
251
- }
252
- }
253
- document.addEventListener('keydown', keydownListener); // In Safari, the button is not focused
254
- currentTarget.style.cursor = 'grabbing';
255
- activeDragRef.current = {
256
- state: 'is-dragging',
257
- startColumnIndex: columnIndex,
258
- startClientX,
259
- headerTopClientY,
260
- columnsClientX,
261
- indicatorHeight,
262
- cleanup: () => {
263
- document.removeEventListener('keydown', keydownListener);
264
- currentTarget.releasePointerCapture(pointerId);
265
- currentTarget.style.removeProperty('cursor');
266
- },
267
- };
268
- renderDragFrame(startClientX);
269
- e.preventDefault();
270
- }, [animateNoChangeThenCleanup, columnIndex, onPointerDown, renderDragFrame]);
271
- const handlePointerMove = useCallback((e) => {
272
- onPointerMove?.(e);
273
- if (e.defaultPrevented || activeDragRef.current?.state !== 'is-dragging')
274
- return;
275
- const { clientX } = e;
276
- renderDragFrame(clientX);
277
- translateXButton(clientX);
278
- const insertionPoint = getClosestInsertionPoint(clientX);
279
- if (insertionPoint)
280
- renderInsertionIndicator(insertionPoint);
281
- }, [getClosestInsertionPoint, translateXButton, onPointerMove, renderDragFrame, renderInsertionIndicator]);
282
- const handlePointerUp = useCallback((e) => {
283
- onPointerUp?.(e);
284
- if (e.defaultPrevented || activeDragRef.current?.state !== 'is-dragging')
285
- return;
286
- const table = e.currentTarget.closest('table');
287
- if (!table) {
288
- cleanupDrag();
289
- return;
290
- }
291
- const { index, after } = getClosestInsertionPoint(e.clientX);
292
- const insertionIndex = after ? index + 1 : index;
293
- if (insertionIndex === columnIndex || insertionIndex === columnIndex + 1) {
294
- animateNoChangeThenCleanup();
295
- return;
296
- }
297
- cleanupDrag();
298
- expectColumnReorder({ fromIndex: columnIndex, insertionIndex });
299
- tableProps.onColumnReorder?.(columnIndex, insertionIndex, tableProps.columns);
300
- }, [
301
- animateNoChangeThenCleanup,
302
- cleanupDrag,
303
- columnIndex,
304
- expectColumnReorder,
305
- getClosestInsertionPoint,
306
- onPointerUp,
307
- tableProps,
308
- ]);
309
- const handlePointerCancel = useCallback((e) => {
310
- onPointerCancel?.(e);
311
- if (e.defaultPrevented)
312
- return;
313
- animateNoChangeThenCleanup();
314
- }, [animateNoChangeThenCleanup, onPointerCancel]);
315
- const handleLostPointerCapture = useCallback((e) => {
316
- onLostPointerCapture?.(e);
317
- if (e.defaultPrevented)
318
- return;
319
- animateNoChangeThenCleanup();
320
- }, [animateNoChangeThenCleanup, onLostPointerCapture]);
321
- const handleKeyDown = useCallback((e) => {
322
- onKeyDown?.(e);
323
- if (e.defaultPrevented || !tableProps)
324
- return;
325
- const left = e.key === 'ArrowLeft';
326
- const right = e.key === 'ArrowRight';
327
- if (!left && !right)
328
- return;
329
- const initialInsertionIndex = left ? columnIndex - 1 : columnIndex + 2;
330
- const step = left ? -1 : 1;
331
- // eslint-disable-next-line yoda
332
- for (let i = initialInsertionIndex; 0 <= i && i <= tableProps.columns.length; i += step) {
333
- if (canReorderCb(i)) {
334
- expectColumnReorder({ fromIndex: columnIndex, insertionIndex: i });
335
- tableProps.onColumnReorder?.(columnIndex, i, tableProps.columns);
336
- e.preventDefault();
337
- return;
338
- }
339
- }
340
- }, [canReorderCb, columnIndex, expectColumnReorder, onKeyDown, tableProps]);
341
- if (!tableProps || !column) {
342
- return null;
343
- }
344
- const hint = `Reorder column ${column.name ?? String(column.key)}.`;
345
- const description = 'Use Left and Right arrow keys to move the column.';
346
- return (
347
- // eslint-disable-next-line jsx-a11y/role-supports-aria-props
348
- <button ref={composedRef} type='button' className={classNames(styles.headerButton, styles.columnReorderHandle, className)} aria-label={hint} aria-description={description} aria-keyshortcuts='ArrowLeft ArrowRight' onKeyDown={handleKeyDown} onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerCancel={handlePointerCancel} onLostPointerCapture={handleLostPointerCapture} {...restProps}>
349
- <Icon glyph={dragIcon} aria-hidden='true'/>
350
- </button>);
135
+ function ColumnReorderHandle({ columnIndex, ...restProps }) {
136
+ return <ReorderHandle direction='columns' index={columnIndex} {...restProps}/>;
351
137
  }
352
138
  /**
353
139
  * Reserves the space for the reorder handle and prevents layout shift when the handle appears on hover.
@@ -1,4 +1,5 @@
1
1
  import { type RefObject } from 'react';
2
+ import { type IntersectionObserverHandle } from '../../global/intersection-observer-context';
2
3
  export type VirtualItem = MaterializedItem | Spacer;
3
4
  interface MaterializedItem {
4
5
  type: 'materialized';
@@ -12,7 +13,11 @@ interface Spacer {
12
13
  key: string;
13
14
  }
14
15
  type CollapseItemIntoSpacerCallback = (index: number, height: number) => void;
15
- export declare const CollapseItemIntoSpacerContext: import("react").Context<CollapseItemIntoSpacerCallback>;
16
+ interface VirtualizationContextValue {
17
+ intersectionObserverHandle: IntersectionObserverHandle;
18
+ collapseItemIntoSpacer: CollapseItemIntoSpacerCallback;
19
+ }
20
+ export declare const VirtualizationContext: import("react").Context<VirtualizationContextValue>;
16
21
  export declare function useVirtualItems<T>({ enabled, data, data: { length }, scrollerRef, tableRef, estimateHeight, lookaheadPx, retentionMarginPx, minScrollAndResizeDeltaPx, }: {
17
22
  enabled: boolean;
18
23
  data: readonly T[];
@@ -24,8 +29,10 @@ export declare function useVirtualItems<T>({ enabled, data, data: { length }, sc
24
29
  minScrollAndResizeDeltaPx: number;
25
30
  }): {
26
31
  virtualItems: VirtualItem[];
27
- intersectionObserverHandle: import("../../global/intersection-observer-context").IntersectionObserverHandle;
28
- collapseItemIntoSpacer: CollapseItemIntoSpacerCallback;
32
+ virtualizationContextValue: {
33
+ intersectionObserverHandle: IntersectionObserverHandle;
34
+ collapseItemIntoSpacer: (index: number, height: number) => void;
35
+ };
29
36
  };
30
37
  export declare function SpacerRow({ spacer: { from, to, height }, colSpan }: {
31
38
  spacer: Spacer;
@@ -1,8 +1,11 @@
1
1
  import { createContext, useCallback, useEffect, useRef, useState } from 'react';
2
- import { useIntersectionObserverHandle } from '../../global/intersection-observer-context';
2
+ import { useIntersectionObserverHandle, } from '../../global/intersection-observer-context';
3
3
  import { setTimeoutWithCleanup } from '../../global/schedule-with-cleanup';
4
4
  import styles from '../table.css';
5
- export const CollapseItemIntoSpacerContext = createContext(() => { });
5
+ export const VirtualizationContext = createContext({
6
+ intersectionObserverHandle: { observe: () => () => { } },
7
+ collapseItemIntoSpacer: () => { },
8
+ });
6
9
  /**
7
10
  * RAF is somewhat too frequent. Most updates happen on virtualization boundaries
8
11
  * within the invisible overscroll area, so they don't require such a high update rate.
@@ -151,16 +154,15 @@ export function useVirtualItems({ enabled, data, data: { length }, scrollerRef,
151
154
  throttle,
152
155
  ]);
153
156
  const intersectionObserverHandle = useIntersectionObserverHandle(scrollerRef, scrollerRef ? retentionMarginPx : undefined, !scrollerRef ? retentionMarginPx : undefined);
154
- const collapseItemIntoSpacer = useCallback((index, height) => {
157
+ function collapseItemIntoSpacer(index, height) {
155
158
  if (!enabled)
156
159
  return;
157
160
  itemsMaterialization.current[index] = height;
158
161
  throttle(recomputeVirtualItems);
159
- }, [enabled, throttle, recomputeVirtualItems]);
162
+ }
160
163
  return {
161
164
  virtualItems,
162
- intersectionObserverHandle,
163
- collapseItemIntoSpacer,
165
+ virtualizationContextValue: { intersectionObserverHandle, collapseItemIntoSpacer },
164
166
  };
165
167
  }
166
168
  function getAllVirtualizedItems(data, estimateHeight) {
@@ -13,13 +13,15 @@ export declare function useItemVirtualization({ index, refs, onIntersectionChang
13
13
  * When multiple elements are provided, the virtualization callback receives
14
14
  * the intersection state of all of them.
15
15
  *
16
- * If you pass multiple refs, memoize the array (for example with `useMemo()`)
17
- * to avoid restarting observation on every render.
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.
18
19
  */
19
20
  refs: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[];
20
21
  /**
21
22
  * Invoked when the `isIntersecting` state of the observed elements changes.
22
- * Consider wrapping a callback to `useCallback()` to avoid restarting observation on every render.
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.
23
25
  *
24
26
  * @param intersectionStates - Current intersection state of every observed element.
25
27
  * Entries are initially `undefined` until the corresponding element
@@ -1,12 +1,10 @@
1
1
  import { use, useEffect } from 'react';
2
- import { IntersectionObserverContext } from '../global/intersection-observer-context';
3
- import { CollapseItemIntoSpacerContext } from './internal/virtual-items';
2
+ import { VirtualizationContext } from './internal/virtualization';
4
3
  /**
5
4
  * Use in an item renderer to control item virtualization.
6
5
  */
7
6
  export function useItemVirtualization({ index, refs, onIntersectionChange, }) {
8
- const handle = use(IntersectionObserverContext);
9
- const collapseItemIntoSpacer = use(CollapseItemIntoSpacerContext);
7
+ const { intersectionObserverHandle, collapseItemIntoSpacer } = use(VirtualizationContext);
10
8
  useEffect(() => {
11
9
  const intersectionStates = Array.isArray(refs) ? refs.map(() => undefined) : [undefined];
12
10
  const elements = Array.isArray(refs) ? refs.map(r => r.current) : [refs.current];
@@ -14,7 +12,7 @@ export function useItemVirtualization({ index, refs, onIntersectionChange, }) {
14
12
  elements.forEach((element, elementIndex) => {
15
13
  if (!element)
16
14
  return;
17
- const cleanup = handle.observe(element, isIntersecting => {
15
+ const cleanup = intersectionObserverHandle.observe(element, isIntersecting => {
18
16
  intersectionStates[elementIndex] = isIntersecting;
19
17
  const height = onIntersectionChange(intersectionStates, elementIndex, elements);
20
18
  if (height != null) {
@@ -24,5 +22,5 @@ export function useItemVirtualization({ index, refs, onIntersectionChange, }) {
24
22
  cleanups.push(cleanup);
25
23
  });
26
24
  return () => cleanups.forEach(cleanup => cleanup());
27
- }, [collapseItemIntoSpacer, handle, index, onIntersectionChange, refs]);
25
+ }, [collapseItemIntoSpacer, intersectionObserverHandle, index, onIntersectionChange, refs]);
28
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
+ }
@@ -4,38 +4,6 @@ import type { TableProps } from './table-props';
4
4
  * Cast to `Context<TableProps<T>>` in usage place.
5
5
  */
6
6
  export declare const TablePropsContext: import("react").Context<TableProps<unknown> | null>;
7
- /**
8
- * Information about a column reorder animation.
9
- *
10
- * Available through {@link ColumnAnimationContext} to allow custom cell
11
- * renderers to animate reordered columns consistently with the default
12
- * table renderer.
13
- */
14
- export interface ColumnAnimation {
15
- /**
16
- * Index of the column being animated.
17
- */
18
- columnIndex: number;
19
- /**
20
- * Current animation phase.
21
- */
22
- phase: 'initial' | 'fade-out';
23
- /**
24
- * CSS class to apply to the animated cell or another element used to
25
- * render the animation.
26
- *
27
- * The class only defines the `background-color` and `transition`
28
- * properties.
29
- */
30
- cellClassName: string;
31
- }
32
- /**
33
- * Provides information about the currently animated column.
34
- *
35
- * Use in a custom cell renderer to animate reordered columns
36
- * consistently with the default table renderer.
37
- */
38
- export declare const ColumnAnimationContext: import("react").Context<ColumnAnimation | null>;
39
7
  /**
40
8
  * When a row only contains unformatted single-line text, it will be exactly of this height.
41
9
  */
@@ -4,13 +4,6 @@ import { createContext } from 'react';
4
4
  * Cast to `Context<TableProps<T>>` in usage place.
5
5
  */
6
6
  export const TablePropsContext = createContext(null);
7
- /**
8
- * Provides information about the currently animated column.
9
- *
10
- * Use in a custom cell renderer to animate reordered columns
11
- * consistently with the default table renderer.
12
- */
13
- export const ColumnAnimationContext = createContext(null);
14
7
  /**
15
8
  * When a row only contains unformatted single-line text, it will be exactly of this height.
16
9
  */