@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.
- package/babel.config.js +1 -1
- package/components/auth-dialog-service/auth-dialog-service.js +2 -2
- package/components/checkbox/checkbox.d.ts +1 -1
- package/components/collapse/collapse-content.js +2 -2
- package/components/collapse/collapse-control.js +2 -2
- package/components/collapse/collapse.js +2 -2
- package/components/collapsible-group/collapsible-group.css +138 -0
- package/components/collapsible-group/collapsible-group.d.ts +20 -0
- package/components/collapsible-group/collapsible-group.js +73 -0
- package/components/confirm-service/confirm-service.js +2 -2
- package/components/data-list/data-list.d.ts +1 -1
- package/components/data-list/data-list.mock.d.ts +1 -1
- package/components/data-list/item.d.ts +1 -1
- package/components/data-list/selection.d.ts +1 -1
- package/components/data-list/selection.js +1 -1
- package/components/date-picker/months.js +2 -2
- package/components/date-picker/use-scroll-behavior.js +5 -6
- package/components/date-picker/years.js +2 -2
- package/components/dialog/dialog.d.ts +2 -2
- package/components/dialog/dialog.js +2 -2
- package/components/dropdown-menu/dropdown-menu.d.ts +4 -4
- package/components/dropdown-menu/dropdown-menu.js +4 -4
- package/components/editable-heading/editable-heading.d.ts +1 -2
- package/components/editable-heading/editable-heading.js +18 -19
- package/components/expand/collapsible-group.css +23 -89
- package/components/expand/collapsible-group.d.ts +8 -16
- package/components/expand/collapsible-group.js +13 -72
- package/components/global/compose-refs.d.ts +2 -1
- package/components/global/compose-refs.js +24 -8
- package/components/global/create-stateful-context.js +5 -5
- package/components/global/focus-with-temporary-tabindex.d.ts +11 -0
- package/components/global/focus-with-temporary-tabindex.js +21 -0
- package/components/global/intersection-observer-context.d.ts +31 -11
- package/components/global/intersection-observer-context.js +47 -28
- package/components/global/is-within-interactive-element.d.ts +6 -0
- package/components/global/is-within-interactive-element.js +26 -0
- package/components/global/is-within-navigable-element.d.ts +6 -0
- package/components/global/is-within-navigable-element.js +27 -0
- package/components/global/parse-css-duration.d.ts +5 -0
- package/components/global/parse-css-duration.js +13 -0
- package/components/global/rerender-hoc.d.ts +4 -2
- package/components/global/rerender-hoc.js +4 -4
- package/components/global/schedule-with-cleanup.d.ts +12 -0
- package/components/global/schedule-with-cleanup.js +34 -0
- package/components/{legacy-table/selection.d.ts → global/table-selection.d.ts} +14 -14
- package/components/{legacy-table/selection.js → global/table-selection.js} +2 -2
- package/components/global/theme.d.ts +4 -3
- package/components/global/theme.js +8 -8
- package/components/i18n/i18n-context.js +1 -1
- package/components/input/input.d.ts +1 -1
- package/components/island/adaptive-island-hoc.js +4 -4
- package/components/island/content.d.ts +7 -2
- package/components/island/content.js +5 -5
- package/components/legacy-table/cell.js +1 -1
- package/components/legacy-table/header-cell.js +1 -1
- package/components/legacy-table/header.js +1 -1
- package/components/legacy-table/multitable.d.ts +1 -1
- package/components/legacy-table/row.d.ts +1 -1
- package/components/legacy-table/row.js +1 -1
- package/components/legacy-table/selection-adapter.d.ts +3 -3
- package/components/legacy-table/selection-shortcuts-hoc.d.ts +5 -5
- package/components/legacy-table/simple-table.d.ts +2 -2
- package/components/legacy-table/simple-table.js +3 -3
- package/components/legacy-table/smart-table.d.ts +5 -5
- package/components/legacy-table/smart-table.js +3 -3
- package/components/legacy-table/table.js +1 -1
- package/components/login-dialog/service.js +2 -2
- package/components/popup/popup.d.ts +2 -0
- package/components/popup/popup.js +2 -2
- package/components/popup/popup.target.d.ts +3 -2
- package/components/popup/popup.target.js +4 -6
- package/components/query-assist/query-assist.d.ts +3 -1
- package/components/query-assist/query-assist.js +2 -2
- package/components/radio/radio-item.d.ts +3 -3
- package/components/radio/radio-item.js +3 -4
- package/components/radio/radio.d.ts +2 -2
- package/components/radio/radio.js +1 -1
- package/components/select/select-popup.d.ts +1 -1
- package/components/select/select.d.ts +4 -2
- package/components/slider/slider.js +4 -5
- package/components/tab-trap/tab-trap.d.ts +3 -3
- package/components/tab-trap/tab-trap.js +3 -5
- package/components/table/default-item-renderer.d.ts +35 -11
- package/components/table/default-item-renderer.js +47 -47
- package/components/table/internal/column-animation.d.ts +16 -0
- package/components/table/internal/column-animation.js +45 -0
- package/components/table/internal/table-header.d.ts +4 -0
- package/components/table/internal/table-header.js +357 -0
- package/components/table/internal/virtual-items.d.ts +34 -0
- package/components/table/{table-virtualize.js → internal/virtual-items.js} +78 -44
- package/components/table/item-virtualization.d.ts +35 -0
- package/components/table/item-virtualization.js +28 -0
- package/components/table/table-const.d.ts +40 -6
- package/components/table/table-const.js +14 -5
- package/components/table/table-primitives.d.ts +21 -0
- package/components/table/table-primitives.js +25 -0
- package/components/table/table-props.d.ts +280 -0
- package/components/table/table-props.js +1 -0
- package/components/table/table.css +171 -66
- package/components/table/table.d.ts +205 -224
- package/components/table/table.js +298 -2
- package/components/tags-input/tags-input.d.ts +3 -1
- package/components/tooltip/tooltip.js +2 -2
- package/components/upload/upload.d.ts +4 -3
- package/components/upload/upload.js +3 -7
- package/components/user-agreement/service.js +2 -2
- package/components/util-stories.d.ts +26 -0
- package/components/util-stories.js +61 -0
- package/package.json +41 -41
- package/components/date-picker/use-intersection-observer.d.ts +0 -6
- package/components/date-picker/use-intersection-observer.js +0 -48
- package/components/global/composeRefs.d.ts +0 -6
- package/components/global/composeRefs.js +0 -7
- package/components/global/use-event-callback.d.ts +0 -1
- package/components/global/use-event-callback.js +0 -15
- package/components/table/table-base.d.ts +0 -24
- package/components/table/table-base.js +0 -79
- package/components/table/table-component.d.ts +0 -53
- package/components/table/table-component.js +0 -101
- package/components/table/table-virtualize.d.ts +0 -32
- /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 {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import styles from '
|
|
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
|
|
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
|
|
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
|
|
28
|
-
|
|
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
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
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 (
|
|
39
|
+
else if (itemTop > materializeEnd) {
|
|
48
40
|
break;
|
|
49
41
|
}
|
|
50
|
-
|
|
42
|
+
currentTop += itemHeight;
|
|
51
43
|
}
|
|
52
44
|
}
|
|
53
|
-
});
|
|
54
|
-
const recomputeVirtualItems =
|
|
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: '
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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 =
|
|
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
|
|
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
|
+
}
|