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