@jetbrains/ring-ui 8.0.0-beta.2 → 8.0.0-beta.4

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 (121) hide show
  1. package/babel.config.js +1 -1
  2. package/components/auth-dialog-service/auth-dialog-service.js +2 -2
  3. package/components/checkbox/checkbox.d.ts +1 -1
  4. package/components/collapse/collapse-content.js +2 -2
  5. package/components/collapse/collapse-control.js +2 -2
  6. package/components/collapse/collapse.js +2 -2
  7. package/components/collapsible-group/collapsible-group.css +138 -0
  8. package/components/collapsible-group/collapsible-group.d.ts +20 -0
  9. package/components/collapsible-group/collapsible-group.js +73 -0
  10. package/components/confirm-service/confirm-service.js +2 -2
  11. package/components/data-list/data-list.d.ts +1 -1
  12. package/components/data-list/data-list.mock.d.ts +1 -1
  13. package/components/data-list/item.d.ts +1 -1
  14. package/components/data-list/selection.d.ts +1 -1
  15. package/components/data-list/selection.js +1 -1
  16. package/components/date-picker/months.js +2 -2
  17. package/components/date-picker/use-scroll-behavior.js +5 -6
  18. package/components/date-picker/years.js +2 -2
  19. package/components/dialog/dialog.d.ts +2 -2
  20. package/components/dialog/dialog.js +2 -2
  21. package/components/dropdown-menu/dropdown-menu.d.ts +4 -4
  22. package/components/dropdown-menu/dropdown-menu.js +4 -4
  23. package/components/editable-heading/editable-heading.d.ts +1 -2
  24. package/components/editable-heading/editable-heading.js +18 -19
  25. package/components/expand/collapsible-group.css +23 -89
  26. package/components/expand/collapsible-group.d.ts +8 -16
  27. package/components/expand/collapsible-group.js +13 -72
  28. package/components/global/compose-refs.d.ts +2 -1
  29. package/components/global/compose-refs.js +24 -8
  30. package/components/global/create-stateful-context.js +5 -5
  31. package/components/global/focus-with-temporary-tabindex.d.ts +11 -0
  32. package/components/global/focus-with-temporary-tabindex.js +21 -0
  33. package/components/global/intersection-observer-context.d.ts +31 -11
  34. package/components/global/intersection-observer-context.js +47 -28
  35. package/components/global/is-within-interactive-element.d.ts +6 -0
  36. package/components/global/is-within-interactive-element.js +26 -0
  37. package/components/global/is-within-navigable-element.d.ts +6 -0
  38. package/components/global/is-within-navigable-element.js +27 -0
  39. package/components/global/parse-css-duration.d.ts +5 -0
  40. package/components/global/parse-css-duration.js +13 -0
  41. package/components/global/rerender-hoc.d.ts +4 -2
  42. package/components/global/rerender-hoc.js +4 -4
  43. package/components/global/schedule-with-cleanup.d.ts +12 -0
  44. package/components/global/schedule-with-cleanup.js +34 -0
  45. package/components/{legacy-table/selection.d.ts → global/table-selection.d.ts} +14 -14
  46. package/components/{legacy-table/selection.js → global/table-selection.js} +2 -2
  47. package/components/global/theme.d.ts +4 -3
  48. package/components/global/theme.js +8 -8
  49. package/components/i18n/i18n-context.js +1 -1
  50. package/components/input/input.d.ts +1 -1
  51. package/components/island/adaptive-island-hoc.js +4 -4
  52. package/components/island/content.d.ts +7 -2
  53. package/components/island/content.js +5 -5
  54. package/components/legacy-table/cell.js +1 -1
  55. package/components/legacy-table/header-cell.js +1 -1
  56. package/components/legacy-table/header.js +1 -1
  57. package/components/legacy-table/multitable.d.ts +1 -1
  58. package/components/legacy-table/row.d.ts +1 -1
  59. package/components/legacy-table/row.js +1 -1
  60. package/components/legacy-table/selection-adapter.d.ts +3 -3
  61. package/components/legacy-table/selection-shortcuts-hoc.d.ts +5 -5
  62. package/components/legacy-table/simple-table.d.ts +2 -2
  63. package/components/legacy-table/simple-table.js +3 -3
  64. package/components/legacy-table/smart-table.d.ts +5 -5
  65. package/components/legacy-table/smart-table.js +3 -3
  66. package/components/legacy-table/table.js +1 -1
  67. package/components/login-dialog/service.js +2 -2
  68. package/components/popup/popup.d.ts +2 -0
  69. package/components/popup/popup.js +2 -2
  70. package/components/popup/popup.target.d.ts +3 -2
  71. package/components/popup/popup.target.js +4 -6
  72. package/components/query-assist/query-assist.d.ts +3 -1
  73. package/components/query-assist/query-assist.js +2 -2
  74. package/components/radio/radio-item.d.ts +3 -3
  75. package/components/radio/radio-item.js +3 -4
  76. package/components/radio/radio.d.ts +2 -2
  77. package/components/radio/radio.js +1 -1
  78. package/components/select/select-popup.d.ts +1 -1
  79. package/components/select/select.d.ts +4 -2
  80. package/components/slider/slider.js +4 -5
  81. package/components/tab-trap/tab-trap.d.ts +3 -3
  82. package/components/tab-trap/tab-trap.js +3 -5
  83. package/components/table/default-item-renderer.d.ts +35 -11
  84. package/components/table/default-item-renderer.js +47 -47
  85. package/components/table/internal/column-animation.d.ts +16 -0
  86. package/components/table/internal/column-animation.js +45 -0
  87. package/components/table/internal/table-header.d.ts +4 -0
  88. package/components/table/internal/table-header.js +357 -0
  89. package/components/table/internal/virtual-items.d.ts +34 -0
  90. package/components/table/{table-virtualize.js → internal/virtual-items.js} +78 -44
  91. package/components/table/item-virtualization.d.ts +35 -0
  92. package/components/table/item-virtualization.js +28 -0
  93. package/components/table/table-const.d.ts +40 -6
  94. package/components/table/table-const.js +14 -5
  95. package/components/table/table-primitives.d.ts +21 -0
  96. package/components/table/table-primitives.js +25 -0
  97. package/components/table/table-props.d.ts +280 -0
  98. package/components/table/table-props.js +1 -0
  99. package/components/table/table.css +171 -66
  100. package/components/table/table.d.ts +205 -224
  101. package/components/table/table.js +298 -2
  102. package/components/tags-input/tags-input.d.ts +3 -1
  103. package/components/tooltip/tooltip.js +2 -2
  104. package/components/upload/upload.d.ts +4 -3
  105. package/components/upload/upload.js +3 -7
  106. package/components/user-agreement/service.js +2 -2
  107. package/components/util-stories.d.ts +26 -0
  108. package/components/util-stories.js +61 -0
  109. package/package.json +41 -41
  110. package/components/date-picker/use-intersection-observer.d.ts +0 -6
  111. package/components/date-picker/use-intersection-observer.js +0 -48
  112. package/components/global/composeRefs.d.ts +0 -6
  113. package/components/global/composeRefs.js +0 -7
  114. package/components/global/use-event-callback.d.ts +0 -1
  115. package/components/global/use-event-callback.js +0 -15
  116. package/components/table/table-base.d.ts +0 -24
  117. package/components/table/table-base.js +0 -79
  118. package/components/table/table-component.d.ts +0 -53
  119. package/components/table/table-component.js +0 -101
  120. package/components/table/table-virtualize.d.ts +0 -32
  121. /package/components/legacy-table/{table.css → legacy-table.css} +0 -0
@@ -0,0 +1,357 @@
1
+ /* eslint-disable no-nested-ternary, max-lines */
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 dragIcon from '@jetbrains/icons/drag-12px';
7
+ import settingsIcon from '@jetbrains/icons/settings-12px';
8
+ import trashIcon from '@jetbrains/icons/trash-12px';
9
+ import unsortedIcon from '@jetbrains/icons/unsorted-12px';
10
+ import { ColumnAnimationContext, TablePropsContext } from '../table-const';
11
+ import Icon from '../../icon';
12
+ import { useComposedRef } from '../../global/compose-refs';
13
+ import { isWithinInteractiveElement } from '../../global/is-within-interactive-element';
14
+ import { parseCssDuration } from '../../global/parse-css-duration';
15
+ import styles from '../table.css';
16
+ export function TableHeader({ expectColumnReorder }) {
17
+ const { columns, noHeader, stickyHeader, columnEditing, onColumnEditingRequest, theadClassName, theadTrClassName } = use(TablePropsContext);
18
+ const [localColumnEditing, setLocalColumnEditing] = useState(false);
19
+ const effectiveColumnEditing = columnEditing ?? localColumnEditing;
20
+ const toggleColumnEditing = useCallback((source) => {
21
+ let newColumnEditing;
22
+ if (columnEditing == null) {
23
+ newColumnEditing = !localColumnEditing;
24
+ setLocalColumnEditing(newColumnEditing);
25
+ }
26
+ else {
27
+ newColumnEditing = !columnEditing;
28
+ }
29
+ onColumnEditingRequest?.(newColumnEditing, source);
30
+ }, [columnEditing, localColumnEditing, onColumnEditingRequest]);
31
+ const handleTheadClick = useCallback((e) => {
32
+ if (window.matchMedia('(hover: none)').matches && !isWithinInteractiveElement(e.target)) {
33
+ toggleColumnEditing('header');
34
+ }
35
+ }, [toggleColumnEditing]);
36
+ const handleEditColumnsButtonClick = useCallback(() => {
37
+ toggleColumnEditing('edit-button');
38
+ }, [toggleColumnEditing]);
39
+ if (noHeader)
40
+ return null;
41
+ return (
42
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events
43
+ <thead className={classNames(theadClassName, effectiveColumnEditing && styles.theadColumnEditing, stickyHeader && styles.stickyHeader)} onClick={handleTheadClick}>
44
+ <tr className={theadTrClassName}>
45
+ {columns.map((column, columnIndex) => (<TableHeaderCell key={column.key} columnIndex={columnIndex} columnEditing={effectiveColumnEditing} handleEditColumnsButtonClick={handleEditColumnsButtonClick} expectColumnReorder={expectColumnReorder}/>))}
46
+ </tr>
47
+ </thead>);
48
+ }
49
+ function TableHeaderCell({ columnIndex, columnEditing, handleEditColumnsButtonClick, expectColumnReorder, }) {
50
+ const { columns, columnEditButton } = use(TablePropsContext);
51
+ const { key, name, renderHeader, sortOrder, deletable, canReorder, thClassName } = columns[columnIndex];
52
+ const animatedColumn = use(ColumnAnimationContext);
53
+ 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}>
55
+ <div className={styles.headerCellInnerWrapper}>
56
+ <div className={styles.sortAndHeader}>
57
+ {canReorder && <ColumnReorderHandle columnIndex={columnIndex} expectColumnReorder={expectColumnReorder}/>}
58
+ {sortOrder ? (<SortButton columnIndex={columnIndex} aria-description={`Sort order: ${sortOrder}`}>
59
+ {children}
60
+ </SortButton>) : (children)}
61
+ {canReorder && <ColumnReorderHandleMirror />}
62
+ </div>
63
+
64
+ <div className={styles.rightButtons}>
65
+ {deletable && <DeleteColumnButton columnIndex={columnIndex}/>}
66
+ {columnIndex === columns.length - 1 && columnEditButton && (<EditColumnsButton columnEditing={columnEditing} onClick={handleEditColumnsButtonClick}/>)}
67
+ </div>
68
+ </div>
69
+ </th>);
70
+ }
71
+ function SortButton({ columnIndex, className, children, onClick, ...restProps }) {
72
+ const tableProps = use(TablePropsContext);
73
+ const column = tableProps?.columns[columnIndex];
74
+ const sortOrder = column?.sortOrder;
75
+ const glyph = sortOrder === 'none'
76
+ ? unsortedIcon
77
+ : sortOrder === 'ascending'
78
+ ? arrowUpIcon
79
+ : sortOrder === 'descending'
80
+ ? arrowDownIcon
81
+ : undefined;
82
+ const handleClick = useCallback((e) => {
83
+ onClick?.(e);
84
+ if (!e.defaultPrevented) {
85
+ const nextOrder = sortOrder === 'ascending' ? 'descending' : sortOrder === 'descending' ? 'none' : 'ascending';
86
+ tableProps.onSort?.(columnIndex, nextOrder, tableProps.columns);
87
+ }
88
+ }, [columnIndex, onClick, sortOrder, tableProps]);
89
+ if (!tableProps || !column) {
90
+ return null;
91
+ }
92
+ return (<button type='button' className={classNames(styles.headerButton, className)} onClick={handleClick} {...restProps}>
93
+ {children} <Icon glyph={glyph} aria-hidden/>
94
+ </button>);
95
+ }
96
+ function DeleteColumnButton({ columnIndex, className, onClick, ...restProps }) {
97
+ const tableProps = use(TablePropsContext);
98
+ const column = tableProps?.columns[columnIndex];
99
+ const handleClick = useCallback((e) => {
100
+ onClick?.(e);
101
+ if (!e.defaultPrevented) {
102
+ tableProps.onColumnDelete?.(columnIndex, tableProps.columns);
103
+ }
104
+ }, [columnIndex, onClick, tableProps]);
105
+ if (!tableProps || !column) {
106
+ return null;
107
+ }
108
+ 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}>
110
+ <Icon glyph={trashIcon} aria-hidden='true'/>
111
+ </button>);
112
+ }
113
+ function EditColumnsButton({ columnEditing, ...props }) {
114
+ const { className, ...restProps } = props;
115
+ 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}>
117
+ <Icon glyph={settingsIcon} aria-hidden='true'/>
118
+ </button>);
119
+ }
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>);
351
+ }
352
+ /**
353
+ * Reserves the space for the reorder handle and prevents layout shift when the handle appears on hover.
354
+ */
355
+ function ColumnReorderHandleMirror() {
356
+ return <span className={styles.columnReorderHandleMirror} aria-hidden='true'/>;
357
+ }
@@ -0,0 +1,34 @@
1
+ import { type RefObject } from 'react';
2
+ export type VirtualItem = MaterializedItem | Spacer;
3
+ interface MaterializedItem {
4
+ type: 'materialized';
5
+ index: number;
6
+ }
7
+ interface Spacer {
8
+ type: 'spacer';
9
+ from: number;
10
+ to: number;
11
+ height: number;
12
+ key: string;
13
+ }
14
+ type CollapseItemIntoSpacerCallback = (index: number, height: number) => void;
15
+ export declare const CollapseItemIntoSpacerContext: import("react").Context<CollapseItemIntoSpacerCallback>;
16
+ export declare function useVirtualItems<T>({ enabled, data, data: { length }, scrollerRef, tableRef, estimateHeight, lookaheadPx, retentionMarginPx, minScrollAndResizeDeltaPx, }: {
17
+ enabled: boolean;
18
+ data: readonly T[];
19
+ scrollerRef: RefObject<HTMLElement | null> | undefined;
20
+ tableRef: RefObject<HTMLTableElement | null>;
21
+ estimateHeight: (item: T, index: number, items: readonly T[]) => number;
22
+ lookaheadPx: number;
23
+ retentionMarginPx: number;
24
+ minScrollAndResizeDeltaPx: number;
25
+ }): {
26
+ virtualItems: VirtualItem[];
27
+ intersectionObserverHandle: import("../../global/intersection-observer-context").IntersectionObserverHandle;
28
+ collapseItemIntoSpacer: CollapseItemIntoSpacerCallback;
29
+ };
30
+ export declare function SpacerRow({ spacer: { from, to, height }, colSpan }: {
31
+ spacer: Spacer;
32
+ colSpan: number;
33
+ }): import("react").JSX.Element;
34
+ export {};
@@ -1,68 +1,60 @@
1
- import { useEffect, useMemo, useRef, useState } from 'react';
2
- import useEventCallback from '../global/use-event-callback';
3
- import { useIntersectionObserverHandle } from '../global/intersection-observer-context';
4
- 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 CollapseItemIntoSpacerContext = createContext(() => { });
5
6
  /**
6
7
  * RAF is somewhat too frequent. Most updates happen on virtualization boundaries
7
8
  * within the invisible overscroll area, so they don't require such a high update rate.
8
9
  * Therefore, we throttle with a custom delay.
9
10
  */
10
11
  const virtualizationThrottleDelay = 50;
11
- export function useTableVirtualize({ enabled, length, scrollerRef, tableRef, estimateHeight, lookaheadPx, retentionMarginPx, minScrollAndResizeDeltaPx, }) {
12
+ export function useVirtualItems({ enabled, data, data: { length }, scrollerRef, tableRef, estimateHeight, lookaheadPx, retentionMarginPx, minScrollAndResizeDeltaPx, }) {
12
13
  const itemsMaterialization = useRef([]);
13
- const [virtualItems, setVirtualItems] = useState(() => [
14
- {
15
- type: 'spacer',
16
- from: 0,
17
- to: length,
18
- height: Array.from({ length }, (_, i) => estimateHeight(i)).reduce((a, b) => a + b, 0),
19
- key: `${styles.spacerRow}-0`,
20
- },
21
- ]);
22
- const materializeVisibleSpacerItems = useEventCallback(() => {
14
+ const [virtualItems, setVirtualItems] = useState(() => enabled ? getAllVirtualizedItems(data, estimateHeight) : []);
15
+ const materializeVisibleSpacerItems = useCallback(() => {
23
16
  if (!tableRef.current)
24
17
  return;
25
- const containerHeight = scrollerRef?.current?.clientHeight ?? window.innerHeight;
18
+ const scrollerRect = scrollerRef?.current?.getBoundingClientRect();
19
+ const visibleStart = scrollerRect?.top ?? 0;
20
+ const visibleEnd = scrollerRect ? Math.min(scrollerRect.bottom, window.innerHeight) : window.innerHeight;
21
+ const materializeStart = visibleStart - lookaheadPx;
22
+ const materializeEnd = visibleEnd + lookaheadPx;
26
23
  for (const spacerRow of tableRef.current.querySelectorAll(`.${styles.spacerRow}`)) {
27
- const rect = spacerRow.getBoundingClientRect();
28
- const spacerIntersectsLookaheadArea = rect.top < containerHeight + lookaheadPx && rect.bottom > -lookaheadPx;
29
- if (!spacerIntersectsLookaheadArea) {
24
+ const spacerRect = spacerRow.getBoundingClientRect();
25
+ if (spacerRect.bottom < materializeStart || spacerRect.top > materializeEnd) {
30
26
  continue;
31
27
  }
32
- const visibleOffsetStart = Math.max(0, -rect.top);
33
- const visibleOffsetEnd = Math.min(rect.height, containerHeight - rect.top);
34
- const materializeOffsetStart = visibleOffsetStart - lookaheadPx;
35
- const materializeOffsetEnd = visibleOffsetEnd + lookaheadPx;
36
- let offsetInSpacer = 0;
37
28
  const from = Number(spacerRow.dataset.from);
38
29
  const to = Number(spacerRow.dataset.to);
30
+ let currentTop = spacerRect.top;
39
31
  for (let i = from; i < to; i++) {
40
32
  const itemMaterialization = itemsMaterialization.current[i];
41
- const itemHeight = typeof itemMaterialization === 'number' ? itemMaterialization : estimateHeight(i);
42
- const itemOffsetStart = offsetInSpacer;
43
- const itemOffsetEnd = offsetInSpacer + itemHeight;
44
- if (itemOffsetStart < materializeOffsetEnd && itemOffsetEnd > materializeOffsetStart) {
33
+ const itemHeight = typeof itemMaterialization === 'number' ? itemMaterialization : estimateHeight(data[i], i, data);
34
+ const itemTop = currentTop;
35
+ const itemBottom = currentTop + itemHeight;
36
+ if (itemBottom >= materializeStart && itemTop <= materializeEnd) {
45
37
  itemsMaterialization.current[i] = true;
46
38
  }
47
- else if (itemOffsetStart > materializeOffsetEnd) {
39
+ else if (itemTop > materializeEnd) {
48
40
  break;
49
41
  }
50
- offsetInSpacer += itemHeight;
42
+ currentTop += itemHeight;
51
43
  }
52
44
  }
53
- });
54
- const recomputeVirtualItems = useEventCallback(() => {
45
+ }, [data, estimateHeight, lookaheadPx, scrollerRef, tableRef]);
46
+ const recomputeVirtualItems = useCallback(() => {
55
47
  const newVirtualItems = [];
56
48
  let spacerCounter = 0;
57
49
  for (let i = 0; i < length; i++) {
58
50
  const itemMaterialization = itemsMaterialization.current[i];
59
51
  if (itemMaterialization === true) {
60
- newVirtualItems.push({ type: 'rendered', index: i });
52
+ newVirtualItems.push({ type: 'materialized', index: i });
61
53
  }
62
54
  else {
63
55
  const lastItemOrSpacer = newVirtualItems[newVirtualItems.length - 1];
64
56
  const lastSpacer = lastItemOrSpacer?.type === 'spacer' ? lastItemOrSpacer : undefined;
65
- const height = typeof itemMaterialization === 'number' ? itemMaterialization : estimateHeight(i);
57
+ const height = typeof itemMaterialization === 'number' ? itemMaterialization : estimateHeight(data[i], i, data);
66
58
  if (lastSpacer) {
67
59
  lastSpacer.to = i + 1;
68
60
  lastSpacer.height += height;
@@ -73,16 +65,16 @@ export function useTableVirtualize({ enabled, length, scrollerRef, tableRef, est
73
65
  from: i,
74
66
  to: i + 1,
75
67
  height,
76
- key: `${styles.spacerRow}-${spacerCounter++}`,
68
+ key: spacerKey(spacerCounter++),
77
69
  });
78
70
  }
79
71
  }
80
72
  }
81
73
  setVirtualItems(newVirtualItems);
82
- });
74
+ }, [data, estimateHeight, length]);
83
75
  const timerIdRef = useRef(null);
84
76
  const callbacksRef = useRef(null);
85
- const throttle = useEventCallback((...callbacks) => {
77
+ const throttle = useCallback((...callbacks) => {
86
78
  if (timerIdRef.current != null) {
87
79
  callbacks.forEach(cb => {
88
80
  callbacksRef.current.delete(cb);
@@ -96,8 +88,11 @@ export function useTableVirtualize({ enabled, length, scrollerRef, tableRef, est
96
88
  timerIdRef.current = null;
97
89
  callbacksRef.current = null;
98
90
  }, virtualizationThrottleDelay);
99
- });
91
+ }, []);
92
+ const prevEnabledRef = useRef(enabled);
100
93
  useEffect(() => {
94
+ const prevEnabled = prevEnabledRef.current;
95
+ prevEnabledRef.current = enabled;
101
96
  if (!enabled)
102
97
  return;
103
98
  const scroller = scrollerRef?.current;
@@ -122,27 +117,66 @@ export function useTableVirtualize({ enabled, length, scrollerRef, tableRef, est
122
117
  const resizeObserver = new ResizeObserver(handleViewportChange);
123
118
  const resizeTarget = scroller ?? document.documentElement;
124
119
  resizeObserver.observe(resizeTarget);
125
- handleViewportChange();
120
+ let timeoutCleanup;
121
+ if (!prevEnabled) {
122
+ timeoutCleanup = setTimeoutWithCleanup(() => {
123
+ itemsMaterialization.current = [];
124
+ setVirtualItems(getAllVirtualizedItems(data, estimateHeight));
125
+ handleViewportChange();
126
+ });
127
+ }
128
+ else {
129
+ handleViewportChange();
130
+ }
126
131
  return () => {
132
+ timeoutCleanup?.();
127
133
  scrollTarget.removeEventListener('scroll', handleViewportChange);
128
134
  resizeObserver.unobserve(resizeTarget);
129
135
  resizeObserver.disconnect();
136
+ if (timerIdRef.current != null) {
137
+ window.clearTimeout(timerIdRef.current);
138
+ timerIdRef.current = null;
139
+ callbacksRef.current = null;
140
+ }
130
141
  };
131
- }, [enabled, materializeVisibleSpacerItems, minScrollAndResizeDeltaPx, recomputeVirtualItems, scrollerRef, throttle]);
142
+ }, [
143
+ data,
144
+ enabled,
145
+ estimateHeight,
146
+ length,
147
+ materializeVisibleSpacerItems,
148
+ minScrollAndResizeDeltaPx,
149
+ recomputeVirtualItems,
150
+ scrollerRef,
151
+ throttle,
152
+ ]);
132
153
  const intersectionObserverHandle = useIntersectionObserverHandle(scrollerRef, scrollerRef ? retentionMarginPx : undefined, !scrollerRef ? retentionMarginPx : undefined);
133
- const collapseItemIntoSpacer = useEventCallback((index, height) => {
154
+ const collapseItemIntoSpacer = useCallback((index, height) => {
134
155
  if (!enabled)
135
156
  return;
136
157
  itemsMaterialization.current[index] = height;
137
158
  throttle(recomputeVirtualItems);
138
- });
139
- const allVisibleVirtualItems = useMemo(() => Array.from({ length: enabled ? 0 : length }, (_, index) => ({ type: 'rendered', index })), [enabled, length]);
159
+ }, [enabled, throttle, recomputeVirtualItems]);
140
160
  return {
141
- virtualItems: enabled ? virtualItems : allVisibleVirtualItems,
161
+ virtualItems,
142
162
  intersectionObserverHandle,
143
163
  collapseItemIntoSpacer,
144
164
  };
145
165
  }
166
+ function getAllVirtualizedItems(data, estimateHeight) {
167
+ return [
168
+ {
169
+ type: 'spacer',
170
+ from: 0,
171
+ to: data.length,
172
+ height: data.reduce((acc, item, index, items) => acc + estimateHeight(item, index, items), 0),
173
+ key: spacerKey(0),
174
+ },
175
+ ];
176
+ }
177
+ function spacerKey(index) {
178
+ return `__${styles.spacerRow}-${index}`;
179
+ }
146
180
  export function SpacerRow({ spacer: { from, to, height }, colSpan }) {
147
181
  return (<tr className={styles.spacerRow} data-from={from} data-to={to}>
148
182
  <td className={styles.spacerCell} colSpan={colSpan} style={{ height }}/>
@@ -0,0 +1,35 @@
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, memoize the array (for example with `useMemo()`)
17
+ * to avoid restarting observation on every render.
18
+ */
19
+ refs: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[];
20
+ /**
21
+ * 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
+ *
24
+ * @param intersectionStates - Current intersection state of every observed element.
25
+ * Entries are initially `undefined` until the corresponding element
26
+ * has been reported by `IntersectionObserver`.
27
+ * @param changedIndex - Index of the element whose intersection state changed.
28
+ * @param elements - The observed elements. Note that some elements may
29
+ * already be disconnected from the DOM when this callback is invoked.
30
+ * Use additional checks like `element.isConnected`.
31
+ * @returns Return the height of an item to collapse the item into spacer,
32
+ * or `undefined` to keep the item rendered.
33
+ */
34
+ onIntersectionChange: (intersectionStates: (boolean | undefined)[], changedIndex: number, elements: (Element | null)[]) => number | undefined;
35
+ }): void;
@@ -0,0 +1,28 @@
1
+ import { use, useEffect } from 'react';
2
+ import { IntersectionObserverContext } from '../global/intersection-observer-context';
3
+ import { CollapseItemIntoSpacerContext } from './internal/virtual-items';
4
+ /**
5
+ * Use in an item renderer to control item virtualization.
6
+ */
7
+ export function useItemVirtualization({ index, refs, onIntersectionChange, }) {
8
+ const handle = use(IntersectionObserverContext);
9
+ const collapseItemIntoSpacer = use(CollapseItemIntoSpacerContext);
10
+ useEffect(() => {
11
+ const intersectionStates = Array.isArray(refs) ? refs.map(() => undefined) : [undefined];
12
+ const elements = Array.isArray(refs) ? refs.map(r => r.current) : [refs.current];
13
+ const cleanups = [];
14
+ elements.forEach((element, elementIndex) => {
15
+ if (!element)
16
+ return;
17
+ const cleanup = handle.observe(element, isIntersecting => {
18
+ intersectionStates[elementIndex] = isIntersecting;
19
+ const height = onIntersectionChange(intersectionStates, elementIndex, elements);
20
+ if (height != null) {
21
+ collapseItemIntoSpacer(index, height);
22
+ }
23
+ });
24
+ cleanups.push(cleanup);
25
+ });
26
+ return () => cleanups.forEach(cleanup => cleanup());
27
+ }, [collapseItemIntoSpacer, handle, index, onIntersectionChange, refs]);
28
+ }