@jetbrains/ring-ui 7.0.115 → 8.0.0-beta.2
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/data-list/data-list.d.ts +4 -4
- package/components/data-list/data-list.js +2 -2
- 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/month.d.ts +0 -2
- package/components/date-picker/month.js +5 -5
- package/components/date-picker/months.js +8 -7
- package/components/date-picker/years.js +11 -10
- package/components/global/intersection-observer-context.d.ts +26 -0
- package/components/global/intersection-observer-context.js +72 -0
- package/components/{table → legacy-table}/selection.d.ts +2 -2
- package/components/{table → legacy-table}/selection.js +1 -1
- package/components/legacy-table/table.css +260 -0
- package/components/legacy-table/table.d.ts +109 -0
- package/components/legacy-table/table.js +191 -0
- package/components/table/default-item-renderer.d.ts +25 -0
- package/components/table/default-item-renderer.js +64 -0
- package/components/table/table-base.d.ts +24 -0
- package/components/table/table-base.js +79 -0
- package/components/table/table-component.d.ts +53 -0
- package/components/table/table-component.js +101 -0
- package/components/table/table-const.d.ts +8 -0
- package/components/table/table-const.js +8 -0
- package/components/table/table-virtualize.d.ts +32 -0
- package/components/table/table-virtualize.js +150 -0
- package/components/table/table.css +76 -199
- package/components/table/table.d.ts +221 -104
- package/components/table/table.js +2 -191
- package/package.json +1 -1
- /package/components/{table → legacy-table}/cell.d.ts +0 -0
- /package/components/{table → legacy-table}/cell.js +0 -0
- /package/components/{table → legacy-table}/disable-hover-hoc.d.ts +0 -0
- /package/components/{table → legacy-table}/disable-hover-hoc.js +0 -0
- /package/components/{table → legacy-table}/header-cell.d.ts +0 -0
- /package/components/{table → legacy-table}/header-cell.js +0 -0
- /package/components/{table → legacy-table}/header.d.ts +0 -0
- /package/components/{table → legacy-table}/header.js +0 -0
- /package/components/{table → legacy-table}/multitable.d.ts +0 -0
- /package/components/{table → legacy-table}/multitable.js +0 -0
- /package/components/{table → legacy-table}/row-with-focus-sensor.d.ts +0 -0
- /package/components/{table → legacy-table}/row-with-focus-sensor.js +0 -0
- /package/components/{table → legacy-table}/row.d.ts +0 -0
- /package/components/{table → legacy-table}/row.js +0 -0
- /package/components/{table → legacy-table}/selection-adapter.d.ts +0 -0
- /package/components/{table → legacy-table}/selection-adapter.js +0 -0
- /package/components/{table → legacy-table}/selection-shortcuts-hoc.d.ts +0 -0
- /package/components/{table → legacy-table}/selection-shortcuts-hoc.js +0 -0
- /package/components/{table → legacy-table}/simple-table.d.ts +0 -0
- /package/components/{table → legacy-table}/simple-table.js +0 -0
- /package/components/{table → legacy-table}/smart-table.d.ts +0 -0
- /package/components/{table → legacy-table}/smart-table.js +0 -0
- /package/components/{table → legacy-table}/table.examples2.json +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @name Table
|
|
3
|
+
*/
|
|
4
|
+
import { Component, PureComponent } from 'react';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import classNames from 'classnames';
|
|
7
|
+
import { arrayMove, List } from 'react-movable';
|
|
8
|
+
import focusSensorHOC from '../global/focus-sensor-hoc';
|
|
9
|
+
import getUID from '../global/get-uid';
|
|
10
|
+
import Shortcuts from '../shortcuts/shortcuts';
|
|
11
|
+
import Loader from '../loader/loader';
|
|
12
|
+
import Header from './header';
|
|
13
|
+
import selectionShortcutsHOC from './selection-shortcuts-hoc';
|
|
14
|
+
import disableHoverHOC from './disable-hover-hoc';
|
|
15
|
+
import Row from './row-with-focus-sensor';
|
|
16
|
+
import style from './table.css';
|
|
17
|
+
/**
|
|
18
|
+
* Interactive table with selection and keyboard navigation support.
|
|
19
|
+
*/
|
|
20
|
+
export class Table extends PureComponent {
|
|
21
|
+
static defaultProps = {
|
|
22
|
+
isItemSelectable: () => true,
|
|
23
|
+
loading: false,
|
|
24
|
+
onSort: () => { },
|
|
25
|
+
onReorder: () => { },
|
|
26
|
+
getItemKey: (item) => {
|
|
27
|
+
// Default behavior stays backward compatible: use item's "id" if present
|
|
28
|
+
if ('id' in item) {
|
|
29
|
+
return item.id;
|
|
30
|
+
}
|
|
31
|
+
// If there's no id provided on item and no getKey supplied, fail fast with a clear message
|
|
32
|
+
throw new Error('Table: getItemKey is required when items have no "id" property');
|
|
33
|
+
},
|
|
34
|
+
sortKey: 'id',
|
|
35
|
+
sortOrder: true,
|
|
36
|
+
draggable: false,
|
|
37
|
+
alwaysShowDragHandle: false,
|
|
38
|
+
stickyHeader: true,
|
|
39
|
+
getItemLevel: () => 0,
|
|
40
|
+
getItemClassName: () => null,
|
|
41
|
+
getMetaColumnClassName: () => null,
|
|
42
|
+
getItemDataTest: () => null,
|
|
43
|
+
isItemCollapsible: () => false,
|
|
44
|
+
isParentCollapsible: () => false,
|
|
45
|
+
isItemCollapsed: () => false,
|
|
46
|
+
onItemCollapse: () => { },
|
|
47
|
+
onItemExpand: () => { },
|
|
48
|
+
onItemDoubleClick: () => { },
|
|
49
|
+
onItemClick: () => { },
|
|
50
|
+
remoteSelection: false,
|
|
51
|
+
isDisabledSelectionVisible: () => false,
|
|
52
|
+
getCheckboxTooltip: () => undefined,
|
|
53
|
+
RowComponent: Row,
|
|
54
|
+
wideFirstColumn: false,
|
|
55
|
+
};
|
|
56
|
+
state = {
|
|
57
|
+
shortcutsScope: getUID('ring-table-'),
|
|
58
|
+
userSelectNone: false,
|
|
59
|
+
};
|
|
60
|
+
componentDidMount() {
|
|
61
|
+
document.addEventListener('mouseup', this.onMouseUp);
|
|
62
|
+
}
|
|
63
|
+
componentDidUpdate({ data, selection, onSelect, selectable, remoteSelection }) {
|
|
64
|
+
if (data !== this.props.data && remoteSelection) {
|
|
65
|
+
onSelect(selection.cloneWith({ data: this.props.data }));
|
|
66
|
+
}
|
|
67
|
+
if (!this.props.selectable && this.props.selectable !== selectable) {
|
|
68
|
+
onSelect(selection.resetSelection());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
componentWillUnmount() {
|
|
72
|
+
document.removeEventListener('mouseup', this.onMouseUp);
|
|
73
|
+
}
|
|
74
|
+
onMouseDown = (e) => {
|
|
75
|
+
if (e.shiftKey) {
|
|
76
|
+
this.setState({ userSelectNone: true });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
onMouseUp = () => {
|
|
80
|
+
if (this.state.userSelectNone) {
|
|
81
|
+
this.setState({ userSelectNone: false });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
onRowFocus = (row) => {
|
|
85
|
+
const { selection, onSelect } = this.props;
|
|
86
|
+
onSelect(selection.focus(row));
|
|
87
|
+
};
|
|
88
|
+
onRowSelect = (row, selected) => {
|
|
89
|
+
const { selection, onSelect } = this.props;
|
|
90
|
+
if (selected) {
|
|
91
|
+
onSelect(selection.select(row));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
onSelect(selection.deselect(row));
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
onSortEnd = ({ oldIndex, newIndex }) => {
|
|
98
|
+
const data = arrayMove(this.props.data, oldIndex, newIndex);
|
|
99
|
+
this.props.onReorder({ data, oldIndex, newIndex });
|
|
100
|
+
};
|
|
101
|
+
onCheckboxChange = (e) => {
|
|
102
|
+
const { checked } = e.currentTarget;
|
|
103
|
+
const { selection, onSelect } = this.props;
|
|
104
|
+
if (checked) {
|
|
105
|
+
onSelect(selection.selectAll());
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
onSelect(selection.reset());
|
|
109
|
+
}
|
|
110
|
+
this.restoreFocusWithoutScroll();
|
|
111
|
+
};
|
|
112
|
+
restoreFocusWithoutScroll = () => {
|
|
113
|
+
const { scrollX, scrollY } = window;
|
|
114
|
+
this.props.onFocusRestore();
|
|
115
|
+
window.scrollTo(scrollX, scrollY);
|
|
116
|
+
};
|
|
117
|
+
render() {
|
|
118
|
+
const { data, selection, columns, caption, getItemKey, selectable, focused, isItemSelectable, getItemLevel, getItemClassName, getMetaColumnClassName, getItemDataTest, draggable, alwaysShowDragHandle, dragHandleTitle, loading, onSort, sortKey, sortOrder, loaderClassName, stickyHeader, stickyHeaderOffset, isItemCollapsible, isParentCollapsible, isItemCollapsed, onItemCollapse, onItemExpand, isDisabledSelectionVisible, getCheckboxTooltip, onItemDoubleClick, onItemClick, renderEmpty, RowComponent, renderLoader, } = this.props;
|
|
119
|
+
// NOTE: Do not construct new object per render because it causes all rows rerendering
|
|
120
|
+
const columnsArray = typeof columns === 'function' ? columns(null) : columns;
|
|
121
|
+
const headerProps = {
|
|
122
|
+
caption,
|
|
123
|
+
selectable,
|
|
124
|
+
draggable,
|
|
125
|
+
columns: columnsArray,
|
|
126
|
+
onSort,
|
|
127
|
+
sortKey,
|
|
128
|
+
sortOrder,
|
|
129
|
+
sticky: stickyHeader,
|
|
130
|
+
topStickOffset: stickyHeaderOffset,
|
|
131
|
+
className: this.props.headerClassName,
|
|
132
|
+
};
|
|
133
|
+
const selectedSize = selection.getSelected().size;
|
|
134
|
+
const allSelectedSize = selection.selectAll().getSelected().size;
|
|
135
|
+
headerProps.checked = selectedSize > 0 && selectedSize === allSelectedSize;
|
|
136
|
+
headerProps.onCheckboxChange = this.onCheckboxChange;
|
|
137
|
+
headerProps.checkboxDisabled = this.props.data.length === 0;
|
|
138
|
+
const wrapperClasses = classNames(style.tableWrapper, this.props.wrapperClassName);
|
|
139
|
+
const classes = classNames(this.props.className, {
|
|
140
|
+
[style.table]: true,
|
|
141
|
+
[style.wideFirstColumn]: this.props.wideFirstColumn,
|
|
142
|
+
[style.userSelectNone]: this.state.userSelectNone,
|
|
143
|
+
[style.disabledHover]: this.props.disabledHover,
|
|
144
|
+
});
|
|
145
|
+
const renderList = ({ children, props }) => {
|
|
146
|
+
const empty = (<tr>
|
|
147
|
+
<td colSpan={columnsArray.length || 1} className={style.tableMessage}>
|
|
148
|
+
{renderEmpty ? renderEmpty() : null}
|
|
149
|
+
</td>
|
|
150
|
+
</tr>);
|
|
151
|
+
const tbody = Array.isArray(children) && children.length > 0 ? children : empty;
|
|
152
|
+
return (<table className={classes} data-test='ring-table'>
|
|
153
|
+
<Header {...headerProps}/>
|
|
154
|
+
<tbody {...props} data-test='ring-table-body'>
|
|
155
|
+
{tbody}
|
|
156
|
+
</tbody>
|
|
157
|
+
</table>);
|
|
158
|
+
};
|
|
159
|
+
const renderItem = ({ value, props = {}, isDragged }) => {
|
|
160
|
+
if (value === null || value === undefined) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const { ref, ...restProps } = props;
|
|
164
|
+
const row = (<RowComponent innerRef={ref} level={getItemLevel(value)} item={value} showFocus={selection.isFocused(value)} autofocus={selection.isFocused(value)} focused={focused && selection.isFocused(value)} selectable={selectable && isItemSelectable(value)} selected={selectable && selection.isSelected(value)} onFocus={this.onRowFocus} onSelect={this.onRowSelect} onDoubleClick={onItemDoubleClick} onClick={onItemClick} collapsible={isItemCollapsible(value)} parentCollapsible={isParentCollapsible(value)} collapsed={isItemCollapsed(value)} onCollapse={onItemCollapse} onExpand={onItemExpand} showDisabledSelection={isDisabledSelectionVisible(value)} checkboxTooltip={getCheckboxTooltip(value)} className={classNames(getItemClassName(value), { [style.draggingRow]: isDragged })} metaColumnClassName={getMetaColumnClassName(value)} draggable={draggable} alwaysShowDragHandle={alwaysShowDragHandle} dragHandleTitle={dragHandleTitle} columns={columns} data-test={getItemDataTest(value)} cellClassName={this.props.cellClassName} {...restProps} key={restProps.key ?? getItemKey(value)}/>);
|
|
165
|
+
return isDragged ? (<table style={{ ...props.style }} className={style.draggingTable}>
|
|
166
|
+
<tbody>{row}</tbody>
|
|
167
|
+
</table>) : (row);
|
|
168
|
+
};
|
|
169
|
+
return (<div className={wrapperClasses} data-test='ring-table-wrapper' ref={this.props.innerRef}>
|
|
170
|
+
{focused && <Shortcuts map={this.props.shortcutsMap} scope={this.state.shortcutsScope}/>}
|
|
171
|
+
|
|
172
|
+
{/* Handler detects that user holds Shift key */}
|
|
173
|
+
<div role='presentation' onMouseDown={this.onMouseDown}>
|
|
174
|
+
{draggable ? (<List values={data} renderList={renderList} renderItem={renderItem} onChange={this.onSortEnd}/>) : (renderList({ children: data.map((value, index) => renderItem({ value, index })) }))}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{loading && (<div className={style.loadingOverlay}>
|
|
178
|
+
{renderLoader ? renderLoader(loaderClassName) : <Loader className={loaderClassName}/>}
|
|
179
|
+
</div>)}
|
|
180
|
+
</div>);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const getContainer = () => disableHoverHOC(selectionShortcutsHOC(focusSensorHOC(Table)));
|
|
184
|
+
// eslint-disable-next-line react/no-multi-comp
|
|
185
|
+
export default class TableContainer extends Component {
|
|
186
|
+
// https://stackoverflow.com/a/53882322/6304152
|
|
187
|
+
Table = getContainer();
|
|
188
|
+
render() {
|
|
189
|
+
return <this.Table {...this.props}/>;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
export interface DefaultItemRendererProps {
|
|
3
|
+
/**
|
|
4
|
+
* Installed on the `<tr>` element
|
|
5
|
+
*/
|
|
6
|
+
ref?: React.RefObject<HTMLTableRowElement | null>;
|
|
7
|
+
/**
|
|
8
|
+
* The index of the `data` item to render
|
|
9
|
+
*/
|
|
10
|
+
index: number;
|
|
11
|
+
/**
|
|
12
|
+
* Changes the highlight on hover and applies the pointer cursor.
|
|
13
|
+
* Note that `false` doesn't mean it cannot handle `onClick`.
|
|
14
|
+
*/
|
|
15
|
+
clickable?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* A level of a nested item. Results in an indent for columns with `indent: true`.
|
|
18
|
+
* 0, negative and not set mean no indent.
|
|
19
|
+
*/
|
|
20
|
+
level?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* @see TableProps.renderItem
|
|
24
|
+
*/
|
|
25
|
+
export declare function DefaultItemRenderer<T>({ ref: userRef, index, clickable, level, className, onKeyDown, onBlur, ...restProps }: DefaultItemRendererProps & ComponentPropsWithoutRef<'tr'>): import("react").JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { CollapseItemIntoSpacerContext, TablePropsContext } from './table-const';
|
|
4
|
+
import { useIsIntersectingListener } from '../global/intersection-observer-context';
|
|
5
|
+
import { TableCell, TableRow } from './table-base';
|
|
6
|
+
import styles from './table.css';
|
|
7
|
+
const INDENT_SIZE = 24;
|
|
8
|
+
/**
|
|
9
|
+
* @see TableProps.renderItem
|
|
10
|
+
*/
|
|
11
|
+
export function DefaultItemRenderer({ ref: userRef, index, clickable, level, className, onKeyDown, onBlur, ...restProps }) {
|
|
12
|
+
const selfRef = useRef(null);
|
|
13
|
+
const ref = userRef ?? selfRef;
|
|
14
|
+
const collapseItemIntoSpacer = useContext(CollapseItemIntoSpacerContext);
|
|
15
|
+
useIsIntersectingListener(ref, isIntersecting => {
|
|
16
|
+
if (ref.current && !isIntersecting) {
|
|
17
|
+
collapseItemIntoSpacer(ref.current.getBoundingClientRect().height);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const { data, columns, selection, isItemKeyboardFocusable, onItemFocus } = useContext(TablePropsContext);
|
|
21
|
+
const item = data[index];
|
|
22
|
+
const selected = selection?.isSelected(item);
|
|
23
|
+
function handleKeyDown(e) {
|
|
24
|
+
onKeyDown?.(e);
|
|
25
|
+
if (!e.defaultPrevented && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
|
26
|
+
const step = e.key === 'ArrowUp' ? -1 : 1;
|
|
27
|
+
// eslint-disable-next-line yoda
|
|
28
|
+
for (let i = index + step; 0 <= i && i < data.length; i += step) {
|
|
29
|
+
if (isItemKeyboardFocusable?.(data[i], i, data)) {
|
|
30
|
+
onItemFocus?.(data[i], i, data);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const focused = selection?.isFocused(item);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (focused)
|
|
39
|
+
ref.current?.focus();
|
|
40
|
+
}, [focused, ref]);
|
|
41
|
+
function handleBlur(e) {
|
|
42
|
+
onBlur?.(e);
|
|
43
|
+
if (!e.defaultPrevented && focused) {
|
|
44
|
+
onItemFocus?.(null, -1, data);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return (<TableRow ref={ref} className={classNames(className, clickable && styles.clickableRow, selected && styles.selectedRow)} onKeyDown={handleKeyDown} tabIndex={focused ? 0 : undefined} onBlur={focused ? handleBlur : undefined} {...restProps}>
|
|
48
|
+
{columns.map((column, columnIndex) => (<TableCell key={column.key} className={column.tdClassName?.(item, index, data)} style={column.indent && level != null && level > 0 ? { paddingInlineStart: `${level * INDENT_SIZE}px` } : undefined}>
|
|
49
|
+
{column.renderCell?.(item, index, data) ?? getDefaultCellValue(item, columnIndex)}
|
|
50
|
+
</TableCell>))}
|
|
51
|
+
</TableRow>);
|
|
52
|
+
}
|
|
53
|
+
function getDefaultCellValue(item, columnIndex) {
|
|
54
|
+
if (Array.isArray(item)) {
|
|
55
|
+
return String(item[columnIndex] ?? '');
|
|
56
|
+
}
|
|
57
|
+
if (item !== null && typeof item === 'object') {
|
|
58
|
+
return String(Object.values(item)[columnIndex] ?? '');
|
|
59
|
+
}
|
|
60
|
+
if (columnIndex === 0) {
|
|
61
|
+
return String(item);
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Include it in a column header to make the column sortable.
|
|
4
|
+
* Handle clicks with {@link TableProps.onSort}.
|
|
5
|
+
*/
|
|
6
|
+
export declare function SortButton<T>(props: ComponentPropsWithoutRef<'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>(props: ComponentPropsWithoutRef<'button'>): import("react").JSX.Element | null;
|
|
13
|
+
/**
|
|
14
|
+
* A helper `<tr>` component for a custom {@link TableProps.renderItem} implementations.
|
|
15
|
+
* Applies the standard row classnames.
|
|
16
|
+
*/
|
|
17
|
+
export declare function TableRow(props: {
|
|
18
|
+
ref?: React.Ref<HTMLTableRowElement>;
|
|
19
|
+
} & ComponentPropsWithoutRef<'tr'>): import("react").JSX.Element;
|
|
20
|
+
/**
|
|
21
|
+
* A helper `<td>` component for a custom {@link TableProps.renderItem} implementations.
|
|
22
|
+
* Applies the standard cell classnames, but not data-dependent `tdClassName`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function TableCell(props: ComponentPropsWithoutRef<'td'>): import("react").JSX.Element;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import unsortedIcon from '@jetbrains/icons/unsorted-12px';
|
|
4
|
+
import arrowDownIcon from '@jetbrains/icons/arrow-12px-down';
|
|
5
|
+
import arrowUpIcon from '@jetbrains/icons/arrow-12px-up';
|
|
6
|
+
import trashIcon from '@jetbrains/icons/trash-12px';
|
|
7
|
+
import Icon from '../icon/icon';
|
|
8
|
+
import { ColumnIndexContext, TablePropsContext } from './table-const';
|
|
9
|
+
import styles from './table.css';
|
|
10
|
+
/**
|
|
11
|
+
* Include it in a column header to make the column sortable.
|
|
12
|
+
* Handle clicks with {@link TableProps.onSort}.
|
|
13
|
+
*/
|
|
14
|
+
export function SortButton(props) {
|
|
15
|
+
const tableProps = useContext(TablePropsContext);
|
|
16
|
+
const columnIndex = useContext(ColumnIndexContext);
|
|
17
|
+
const column = tableProps?.columns[columnIndex];
|
|
18
|
+
if (!tableProps || !column) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const sortOrder = column.sortOrder ?? 'none';
|
|
22
|
+
// eslint-disable-next-line no-nested-ternary, prettier/prettier
|
|
23
|
+
const glyph = sortOrder === 'none' ? unsortedIcon
|
|
24
|
+
: sortOrder === 'ascending' ? arrowUpIcon
|
|
25
|
+
: arrowDownIcon;
|
|
26
|
+
const { className, children, onClick, ...restProps } = props;
|
|
27
|
+
function handleClick(e) {
|
|
28
|
+
onClick?.(e);
|
|
29
|
+
if (!e.defaultPrevented) {
|
|
30
|
+
const sequence = ['none', 'ascending', 'descending'];
|
|
31
|
+
const nextOrder = sequence[(sequence.indexOf(sortOrder) + 1) % sequence.length];
|
|
32
|
+
tableProps.onSort?.(columnIndex, nextOrder);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return (<button type='button' className={classNames(styles.headerButton, className)} onClick={handleClick} {...restProps}>
|
|
36
|
+
{children} <Icon glyph={glyph} aria-hidden/>
|
|
37
|
+
</button>);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Include it in a column header to make the column deletable.
|
|
41
|
+
* Beware that `column.name ?? String(column.key)` is used in the aria-label.
|
|
42
|
+
* Handle clicks with {@link TableProps.onColumnDelete}.
|
|
43
|
+
*/
|
|
44
|
+
export function DeleteColumnButton(props) {
|
|
45
|
+
const tableProps = useContext(TablePropsContext);
|
|
46
|
+
const columnIndex = useContext(ColumnIndexContext);
|
|
47
|
+
const column = tableProps?.columns[columnIndex];
|
|
48
|
+
if (!tableProps || !column) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const { className, onClick, ...restProps } = props;
|
|
52
|
+
function handleClick(e) {
|
|
53
|
+
onClick?.(e);
|
|
54
|
+
if (!e.defaultPrevented) {
|
|
55
|
+
tableProps.onColumnDelete?.(columnIndex);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return (<button type='button' className={classNames(styles.headerButton, styles.deleteColumnButton, className)} onClick={handleClick} aria-label={`Delete column ${column.name ?? String(column.key)}`} {...restProps}>
|
|
59
|
+
<Icon glyph={trashIcon}/>
|
|
60
|
+
</button>);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A helper `<tr>` component for a custom {@link TableProps.renderItem} implementations.
|
|
64
|
+
* Applies the standard row classnames.
|
|
65
|
+
*/
|
|
66
|
+
export function TableRow(props) {
|
|
67
|
+
const { ref, className, ...restProps } = props;
|
|
68
|
+
const classes = classNames(styles.row, className);
|
|
69
|
+
return <tr ref={ref} className={classes} {...restProps}/>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A helper `<td>` component for a custom {@link TableProps.renderItem} implementations.
|
|
73
|
+
* Applies the standard cell classnames, but not data-dependent `tdClassName`.
|
|
74
|
+
*/
|
|
75
|
+
export function TableCell(props) {
|
|
76
|
+
const { className, ...restProps } = props;
|
|
77
|
+
const classes = classNames(styles.cell, className);
|
|
78
|
+
return <td className={classes} {...restProps}/>;
|
|
79
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
import type { TableProps } from './table';
|
|
3
|
+
/**
|
|
4
|
+
* The new Table component. Use it instead of tables in the `legacy-table` folder.
|
|
5
|
+
*
|
|
6
|
+
* Minimal usage requires the following props:
|
|
7
|
+
* - `data`
|
|
8
|
+
* - `getKey`
|
|
9
|
+
* - `columns`
|
|
10
|
+
* - `key`
|
|
11
|
+
* - `renderCell` (not required, but usually needed)
|
|
12
|
+
*
|
|
13
|
+
* ## Selection
|
|
14
|
+
*
|
|
15
|
+
* Following three props support the selection:
|
|
16
|
+
*
|
|
17
|
+
* - `selection`
|
|
18
|
+
* - `isItemClickable`
|
|
19
|
+
* - `DefaultItemRenderer.onClick`
|
|
20
|
+
*
|
|
21
|
+
* Only `selection` is required: you can display and modify selection your way, e.g., via
|
|
22
|
+
* checkboxes in cells.
|
|
23
|
+
*
|
|
24
|
+
* ## Sorting
|
|
25
|
+
* You need the following to support sorting:
|
|
26
|
+
*
|
|
27
|
+
* - Include `<SortButton />` in a column header
|
|
28
|
+
* - Set initial `Column.sortOrder` to `none`, `ascending`.
|
|
29
|
+
* Do not leave `undefined` for the accessibility reasons.
|
|
30
|
+
* - Handle `TableProps.onSort` callback in the client code. It is expected
|
|
31
|
+
* to update `columns`, by setting the new `sortOrder` value for
|
|
32
|
+
* the corresponding column, and updating the data accordingly.
|
|
33
|
+
*
|
|
34
|
+
* ## Deleting columns
|
|
35
|
+
* You need the following to support deleting columns:
|
|
36
|
+
*
|
|
37
|
+
* - Make sure the `column` has a proper `name` or `key` prop, which will be
|
|
38
|
+
* automatically included in the aria-label of `<DeleteColumnButton />`.
|
|
39
|
+
* - Include `<DeleteColumnButton />` in a column header
|
|
40
|
+
* - Handle `TableProps.onColumnDelete` callback in the client code. It is expected
|
|
41
|
+
* to update `columns` by removing the corresponding column.
|
|
42
|
+
*
|
|
43
|
+
* ## Row virtualization
|
|
44
|
+
*
|
|
45
|
+
* To render only rows near the viewport and replace others with spacers, use:
|
|
46
|
+
*
|
|
47
|
+
* - `virtualizeRows`
|
|
48
|
+
* - `scrollerRef` — required when the scrollable container is not the whole document
|
|
49
|
+
* - `estimateHeight` — recommended when rows are expected to be taller than
|
|
50
|
+
* the default height (e.g. multiline or custom content)
|
|
51
|
+
* - Fine-tuning props: `lookaheadPx`, `retentionMarginPx`, `minScrollAndResizeDeltaPx`
|
|
52
|
+
*/
|
|
53
|
+
export default function Table<T>(props: TableProps<T> & ComponentPropsWithoutRef<'table'>): import("react").JSX.Element;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { IntersectionObserverContext } from '../global/intersection-observer-context';
|
|
4
|
+
import { SpacerRow, useTableVirtualize } from './table-virtualize';
|
|
5
|
+
import { DefaultItemRenderer } from './default-item-renderer';
|
|
6
|
+
import { CollapseItemIntoSpacerContext, ColumnIndexContext, defaultLookaheadPx, defaultMinScrollAndResizeDeltaPx, defaultRetentionMarginPx, defaultRowHeight, TablePropsContext, } from './table-const';
|
|
7
|
+
import styles from './table.css';
|
|
8
|
+
/**
|
|
9
|
+
* The new Table component. Use it instead of tables in the `legacy-table` folder.
|
|
10
|
+
*
|
|
11
|
+
* Minimal usage requires the following props:
|
|
12
|
+
* - `data`
|
|
13
|
+
* - `getKey`
|
|
14
|
+
* - `columns`
|
|
15
|
+
* - `key`
|
|
16
|
+
* - `renderCell` (not required, but usually needed)
|
|
17
|
+
*
|
|
18
|
+
* ## Selection
|
|
19
|
+
*
|
|
20
|
+
* Following three props support the selection:
|
|
21
|
+
*
|
|
22
|
+
* - `selection`
|
|
23
|
+
* - `isItemClickable`
|
|
24
|
+
* - `DefaultItemRenderer.onClick`
|
|
25
|
+
*
|
|
26
|
+
* Only `selection` is required: you can display and modify selection your way, e.g., via
|
|
27
|
+
* checkboxes in cells.
|
|
28
|
+
*
|
|
29
|
+
* ## Sorting
|
|
30
|
+
* You need the following to support sorting:
|
|
31
|
+
*
|
|
32
|
+
* - Include `<SortButton />` in a column header
|
|
33
|
+
* - Set initial `Column.sortOrder` to `none`, `ascending`.
|
|
34
|
+
* Do not leave `undefined` for the accessibility reasons.
|
|
35
|
+
* - Handle `TableProps.onSort` callback in the client code. It is expected
|
|
36
|
+
* to update `columns`, by setting the new `sortOrder` value for
|
|
37
|
+
* the corresponding column, and updating the data accordingly.
|
|
38
|
+
*
|
|
39
|
+
* ## Deleting columns
|
|
40
|
+
* You need the following to support deleting columns:
|
|
41
|
+
*
|
|
42
|
+
* - Make sure the `column` has a proper `name` or `key` prop, which will be
|
|
43
|
+
* automatically included in the aria-label of `<DeleteColumnButton />`.
|
|
44
|
+
* - Include `<DeleteColumnButton />` in a column header
|
|
45
|
+
* - Handle `TableProps.onColumnDelete` callback in the client code. It is expected
|
|
46
|
+
* to update `columns` by removing the corresponding column.
|
|
47
|
+
*
|
|
48
|
+
* ## Row virtualization
|
|
49
|
+
*
|
|
50
|
+
* To render only rows near the viewport and replace others with spacers, use:
|
|
51
|
+
*
|
|
52
|
+
* - `virtualizeRows`
|
|
53
|
+
* - `scrollerRef` — required when the scrollable container is not the whole document
|
|
54
|
+
* - `estimateHeight` — recommended when rows are expected to be taller than
|
|
55
|
+
* the default height (e.g. multiline or custom content)
|
|
56
|
+
* - Fine-tuning props: `lookaheadPx`, `retentionMarginPx`, `minScrollAndResizeDeltaPx`
|
|
57
|
+
*/
|
|
58
|
+
export default function Table(props) {
|
|
59
|
+
const { data, columns, getKey, selection, isItemKeyboardFocusable, onItemFocus, onItemMove, onSort, onColumnDelete, onColumnMove, renderItem, virtualizeRows = false, scrollerRef, estimateHeight = () => defaultRowHeight, lookaheadPx = defaultLookaheadPx, retentionMarginPx = defaultRetentionMarginPx, minScrollAndResizeDeltaPx = defaultMinScrollAndResizeDeltaPx, columnEditButton, ref: userRef, className, theadClassName, theadTrClassName, tbodyClassName, ...restProps } = props;
|
|
60
|
+
const selfRef = useRef(null);
|
|
61
|
+
const tableRef = userRef ?? selfRef;
|
|
62
|
+
const { virtualItems, intersectionObserverHandle, collapseItemIntoSpacer } = useTableVirtualize({
|
|
63
|
+
enabled: virtualizeRows,
|
|
64
|
+
length: data.length,
|
|
65
|
+
scrollerRef,
|
|
66
|
+
tableRef,
|
|
67
|
+
estimateHeight,
|
|
68
|
+
lookaheadPx,
|
|
69
|
+
retentionMarginPx,
|
|
70
|
+
minScrollAndResizeDeltaPx,
|
|
71
|
+
});
|
|
72
|
+
return (<TablePropsContext.Provider value={props}>
|
|
73
|
+
<IntersectionObserverContext.Provider value={intersectionObserverHandle}>
|
|
74
|
+
<table className={classNames(styles.table, className)} ref={tableRef} {...restProps}>
|
|
75
|
+
<thead className={theadClassName}>
|
|
76
|
+
<tr className={classNames(styles.headerRow, theadTrClassName)}>
|
|
77
|
+
{columns.map((column, columnIndex) => (<th key={column.key} className={classNames(styles.headerCell, column.thClassName)} aria-sort={column.sortOrder}>
|
|
78
|
+
<ColumnIndexContext.Provider value={columnIndex}>
|
|
79
|
+
{column.renderHeader?.() ?? column.name ?? String(column.key)}
|
|
80
|
+
</ColumnIndexContext.Provider>
|
|
81
|
+
</th>))}
|
|
82
|
+
</tr>
|
|
83
|
+
</thead>
|
|
84
|
+
|
|
85
|
+
<tbody className={tbodyClassName}>
|
|
86
|
+
{virtualItems.map(virtualItem => {
|
|
87
|
+
if (virtualItem.type === 'spacer') {
|
|
88
|
+
return <SpacerRow key={virtualItem.key} spacer={virtualItem} colSpan={columns.length}/>;
|
|
89
|
+
}
|
|
90
|
+
const index = virtualItem.index;
|
|
91
|
+
const item = data[index];
|
|
92
|
+
const key = props.getKey(item, index);
|
|
93
|
+
return (<CollapseItemIntoSpacerContext.Provider value={height => collapseItemIntoSpacer(index, height)} key={key}>
|
|
94
|
+
{renderItem ? renderItem(item, index, data) : <DefaultItemRenderer index={index}/>}
|
|
95
|
+
</CollapseItemIntoSpacerContext.Provider>);
|
|
96
|
+
})}
|
|
97
|
+
</tbody>
|
|
98
|
+
</table>
|
|
99
|
+
</IntersectionObserverContext.Provider>
|
|
100
|
+
</TablePropsContext.Provider>);
|
|
101
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TableProps } from './table';
|
|
2
|
+
export declare const TablePropsContext: import("react").Context<TableProps<unknown> | null>;
|
|
3
|
+
export declare const ColumnIndexContext: import("react").Context<number>;
|
|
4
|
+
export declare const CollapseItemIntoSpacerContext: import("react").Context<(height: number) => void>;
|
|
5
|
+
export declare const defaultRowHeight = 37;
|
|
6
|
+
export declare const defaultLookaheadPx = 400;
|
|
7
|
+
export declare const defaultRetentionMarginPx = 450;
|
|
8
|
+
export declare const defaultMinScrollAndResizeDeltaPx = 50;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
export const TablePropsContext = createContext(null);
|
|
3
|
+
export const ColumnIndexContext = createContext(-1);
|
|
4
|
+
export const CollapseItemIntoSpacerContext = createContext(() => { });
|
|
5
|
+
export const defaultRowHeight = 37;
|
|
6
|
+
export const defaultLookaheadPx = 400;
|
|
7
|
+
export const defaultRetentionMarginPx = 450;
|
|
8
|
+
export const defaultMinScrollAndResizeDeltaPx = 50;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
export type VirtualItem = RenderedItem | Spacer;
|
|
3
|
+
interface RenderedItem {
|
|
4
|
+
type: 'rendered';
|
|
5
|
+
index: number;
|
|
6
|
+
}
|
|
7
|
+
interface Spacer {
|
|
8
|
+
type: 'spacer';
|
|
9
|
+
from: number;
|
|
10
|
+
to: number;
|
|
11
|
+
height: number;
|
|
12
|
+
key: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function useTableVirtualize({ enabled, length, scrollerRef, tableRef, estimateHeight, lookaheadPx, retentionMarginPx, minScrollAndResizeDeltaPx, }: {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
length: number;
|
|
17
|
+
scrollerRef: RefObject<HTMLElement | null> | undefined;
|
|
18
|
+
tableRef: RefObject<HTMLTableElement | null>;
|
|
19
|
+
estimateHeight: (index: number) => number;
|
|
20
|
+
lookaheadPx: number;
|
|
21
|
+
retentionMarginPx: number;
|
|
22
|
+
minScrollAndResizeDeltaPx: number;
|
|
23
|
+
}): {
|
|
24
|
+
virtualItems: VirtualItem[];
|
|
25
|
+
intersectionObserverHandle: import("../global/intersection-observer-context").IntersectionObserverHandle | null;
|
|
26
|
+
collapseItemIntoSpacer: (index: number, height: number) => void;
|
|
27
|
+
};
|
|
28
|
+
export declare function SpacerRow({ spacer: { from, to, height }, colSpan }: {
|
|
29
|
+
spacer: Spacer;
|
|
30
|
+
colSpan: number;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
32
|
+
export {};
|