@linagora/twake-mui 10.2.0 → 10.3.0
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/CHANGELOG.md +7 -0
- package/dist/components/VirtualizedTable/Cell.d.ts +1 -1
- package/dist/components/VirtualizedTable/Cell.js +17 -9
- package/dist/components/VirtualizedTable/FixedHeaderContent.d.ts +1 -0
- package/dist/components/VirtualizedTable/FixedHeaderContent.js +1 -1
- package/dist/components/VirtualizedTable/HeadCell.d.ts +1 -0
- package/dist/components/VirtualizedTable/HeadCell.js +7 -4
- package/dist/components/VirtualizedTable/VirtualizedTable.stories.js +15 -5
- package/dist/components/VirtualizedTable/index.d.ts +5 -1
- package/dist/components/VirtualizedTable/index.js +16 -5
- package/dist/components/VirtualizedTable/types.d.ts +3 -1
- package/dist/components/VirtualizedTable/types.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# @linagora/twake-mui [10.3.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@10.2.0...@linagora/twake-mui@10.3.0) (2026-10-06)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **twake-mui:** Implement loading state in virtualized table ([7a043cc](https://github.com/linagora/twake-ui/commit/7a043cc94a1e5af4229387088c2cae3214f8bd20))
|
|
7
|
+
|
|
1
8
|
# @linagora/twake-mui [10.2.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@10.1.0...@linagora/twake-mui@10.2.0) (2026-10-06)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
import { TableCell } from '@mui/material';
|
|
1
|
+
import { TableCell, Skeleton } from '@mui/material';
|
|
2
2
|
import React, { useRef } from 'react';
|
|
3
3
|
import { useOnLongPress } from 'rooks';
|
|
4
4
|
import { getPath } from './helpers';
|
|
5
|
+
import { _isLoading as _isLoadingSymbol } from './types';
|
|
5
6
|
const DOUBLE_CLICK_DELAY = 400;
|
|
6
7
|
const LONG_PRESS_DURATION = 300;
|
|
7
8
|
export const Cell = ({ row, columns, column, onClick, onDoubleClick, onLongPress, children }) => {
|
|
8
9
|
// onClick is triggered after a long press anyway, so we need this flag to swallow it
|
|
9
10
|
const isLongPress = useRef(false);
|
|
10
11
|
const lastClickTime = useRef(0);
|
|
12
|
+
const _isLoading = row[_isLoadingSymbol] === true;
|
|
11
13
|
const cellContent = getPath(row, column.id);
|
|
12
14
|
const cell = cellContent === undefined ? '—' : cellContent;
|
|
13
15
|
const longPressRef = useOnLongPress(() => {
|
|
14
16
|
// without a long press handler, a slow click must stay a click
|
|
15
|
-
if (column.disableClick || !onLongPress)
|
|
17
|
+
if (_isLoading || column.disableClick || !onLongPress)
|
|
16
18
|
return;
|
|
17
19
|
isLongPress.current = true;
|
|
18
20
|
onLongPress(row, column);
|
|
19
21
|
}, { duration: LONG_PRESS_DURATION });
|
|
20
22
|
const handleClick = () => {
|
|
21
|
-
if (column.disableClick)
|
|
23
|
+
if (_isLoading || column.disableClick)
|
|
22
24
|
return;
|
|
23
25
|
if (isLongPress.current) {
|
|
24
26
|
isLongPress.current = false;
|
|
@@ -31,12 +33,18 @@ export const Cell = ({ row, columns, column, onClick, onDoubleClick, onLongPress
|
|
|
31
33
|
onClick?.(row, column);
|
|
32
34
|
};
|
|
33
35
|
return (React.createElement(TableCell, { ref: longPressRef, sx: {
|
|
34
|
-
cursor: !!onClick && !column.disableClick
|
|
36
|
+
cursor: !!onClick && !_isLoading && !column.disableClick
|
|
37
|
+
? 'pointer'
|
|
38
|
+
: undefined,
|
|
35
39
|
width: column.width,
|
|
36
40
|
maxWidth: column.maxWidth
|
|
37
|
-
}, className: column.noWrap ? 'u-ellipsis' : undefined, align: column.textAlign ?? 'left', padding: column.disablePadding ? 'none' : 'normal', onClick: handleClick, onDoubleClick: () =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
:
|
|
41
|
+
}, className: column.noWrap ? 'u-ellipsis' : undefined, align: column.textAlign ?? 'left', padding: column.disablePadding ? 'none' : 'normal', onClick: handleClick, onDoubleClick: () => _isLoading || column.disableClick
|
|
42
|
+
? undefined
|
|
43
|
+
: onDoubleClick?.(row, column), onContextMenu: ev => isLongPress.current && ev.preventDefault() }, _isLoading
|
|
44
|
+
? column.skeleton || React.createElement(Skeleton, { animation: "wave", variant: "text" })
|
|
45
|
+
: children
|
|
46
|
+
? React.Children.map(children, child => React.isValidElement(child)
|
|
47
|
+
? React.cloneElement(child, { row, columns, column, cell })
|
|
48
|
+
: null)
|
|
49
|
+
: cell));
|
|
42
50
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { TableRow } from '@mui/material';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { HeadCell } from './HeadCell';
|
|
4
|
-
export const FixedHeaderContent = ({ columns, orderBy, orderDirection, onClick }) => (React.createElement(TableRow, null, columns.map(column => (React.createElement(HeadCell, { key: column.id, column: column, orderBy: orderBy, orderDirection: orderDirection, onClick: () => onClick(column.id) })))));
|
|
4
|
+
export const FixedHeaderContent = ({ columns, orderBy, orderDirection, onClick, isLoading }) => (React.createElement(TableRow, null, columns.map(column => (React.createElement(HeadCell, { key: column.id, column: column, orderBy: orderBy, orderDirection: orderDirection, onClick: () => onClick(column.id), isLoading: isLoading })))));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TableCell, TableSortLabel } from '@mui/material';
|
|
1
|
+
import { TableCell, TableSortLabel, Skeleton } from '@mui/material';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { useExtendI18n, useI18n } from 'twake-i18n';
|
|
4
4
|
import de from './locales/de.json';
|
|
@@ -9,13 +9,16 @@ import it from './locales/it.json';
|
|
|
9
9
|
import ru from './locales/ru.json';
|
|
10
10
|
import vi from './locales/vi.json';
|
|
11
11
|
const locales = { de, en, es, fr, it, ru, vi };
|
|
12
|
-
export const HeadCell = ({ column, orderBy, orderDirection, onClick }) => {
|
|
12
|
+
export const HeadCell = ({ column, orderBy, orderDirection, onClick, isLoading }) => {
|
|
13
13
|
useExtendI18n(locales);
|
|
14
14
|
const { t } = useI18n();
|
|
15
15
|
const isActive = orderBy === column.id;
|
|
16
|
+
const content = isLoading ? (React.createElement(React.Fragment, null,
|
|
17
|
+
React.createElement("span", { className: "u-visuallyhidden" }, column.label),
|
|
18
|
+
React.createElement(Skeleton, { animation: "wave", width: 50 }))) : (column.label);
|
|
16
19
|
return (React.createElement(TableCell, { sx: { width: column.width, maxWidth: column.maxWidth }, className: column.noWrap ? 'u-ellipsis' : undefined, align: column.textAlign ?? 'left', padding: column.disablePadding ? 'none' : 'normal', sortDirection: isActive ? orderDirection : false }, column.sortable !== false ? (React.createElement(TableSortLabel, { active: isActive, direction: isActive ? orderDirection : 'asc', onClick: onClick },
|
|
17
|
-
|
|
20
|
+
content,
|
|
18
21
|
isActive && (React.createElement("span", { className: "u-visuallyhidden" }, orderDirection === 'desc'
|
|
19
22
|
? t('VirtualizedTable.sortedDesc')
|
|
20
|
-
: t('VirtualizedTable.sortedAsc'))))) : (
|
|
23
|
+
: t('VirtualizedTable.sortedAsc'))))) : (content)));
|
|
21
24
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Button, Typography } from '@mui/material';
|
|
1
|
+
import { Button, Skeleton, Typography } from '@mui/material';
|
|
2
2
|
import React, { useState } from 'react';
|
|
3
3
|
import { fn } from 'storybook/test';
|
|
4
4
|
import { VirtualizedTable } from './index';
|
|
@@ -26,7 +26,8 @@ const columns = [
|
|
|
26
26
|
label: 'Calories',
|
|
27
27
|
width: 80,
|
|
28
28
|
textAlign: 'left',
|
|
29
|
-
sortable: false
|
|
29
|
+
sortable: false,
|
|
30
|
+
skeleton: (React.createElement(Skeleton, { animation: "wave", variant: "circular", width: 24, height: 24 }))
|
|
30
31
|
},
|
|
31
32
|
{ id: 'fat', label: 'Fat (g)', width: 85, textAlign: 'right' },
|
|
32
33
|
{ id: 'carbs', label: 'Carbs (g)', width: 115, textAlign: 'right' },
|
|
@@ -46,15 +47,20 @@ const makeGroups = () => ({
|
|
|
46
47
|
const UNSELECTABLE_ID = 1;
|
|
47
48
|
const onDoubleClick = fn().mockName('onDoubleClick');
|
|
48
49
|
const onLongPress = fn().mockName('onLongPress');
|
|
49
|
-
const ExampleTable = ({ title, ...props }) => {
|
|
50
|
+
const ExampleTable = ({ title, isLoading: propsIsLoading = false, ...props }) => {
|
|
50
51
|
const [selected, setSelected] = useState([]);
|
|
52
|
+
const [localIsLoading, setLocalIsLoading] = useState(propsIsLoading);
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
setLocalIsLoading(propsIsLoading);
|
|
55
|
+
}, [propsIsLoading]);
|
|
51
56
|
const toggle = (id) => setSelected(ids => ids.includes(id) ? ids.filter(i => i !== id) : [...ids, id]);
|
|
52
57
|
const toggleAll = () => setSelected(ids => ids.length ? [] : rows.map(r => r.id).filter(id => id !== 1));
|
|
53
58
|
return (React.createElement(React.Fragment, null,
|
|
54
59
|
React.createElement(Typography, { sx: { mt: 2 }, variant: "h4" }, title),
|
|
55
60
|
React.createElement(Button, { sx: { my: 2 }, variant: "text", onClick: toggleAll }, "Select all"),
|
|
61
|
+
React.createElement(Button, { sx: { my: 2, ml: 2 }, variant: "text", onClick: () => setLocalIsLoading(!localIsLoading) }, "Toggle loading"),
|
|
56
62
|
React.createElement("div", { style: { border: '1px solid #ccc', height: 400, width: '100%' } },
|
|
57
|
-
React.createElement(VirtualizedTable, { rows: rows, columns: columns, selectedItems: selected, isSelectedItem: row => selected.includes(row.id), componentsProps: {
|
|
63
|
+
React.createElement(VirtualizedTable, { rows: rows, columns: columns, isLoading: localIsLoading, selectedItems: selected, isSelectedItem: row => selected.includes(row.id), componentsProps: {
|
|
58
64
|
rowContent: {
|
|
59
65
|
onClick: row => row.id !== UNSELECTABLE_ID && toggle(row.id),
|
|
60
66
|
onDoubleClick: (row, column) => row.id !== UNSELECTABLE_ID && onDoubleClick(row, column),
|
|
@@ -71,6 +77,7 @@ const meta = {
|
|
|
71
77
|
columns: { control: 'object' },
|
|
72
78
|
defaultOrder: { control: 'object' },
|
|
73
79
|
selectedItems: { control: 'object' },
|
|
80
|
+
isLoading: { control: 'boolean' },
|
|
74
81
|
groups: { control: false },
|
|
75
82
|
secondarySort: { control: false },
|
|
76
83
|
isSelectedItem: { control: false },
|
|
@@ -100,5 +107,8 @@ export const Screenshot = {
|
|
|
100
107
|
React.createElement("section", null,
|
|
101
108
|
React.createElement("h3", null, "Grouped"),
|
|
102
109
|
React.createElement(ExampleTable, { title: "Not sorted table", groups: makeGroups }),
|
|
103
|
-
React.createElement(ExampleTable, { title: "Sorted table", groups: makeGroups, defaultOrder: { by: 'name', direction: 'asc' } }))
|
|
110
|
+
React.createElement(ExampleTable, { title: "Sorted table", groups: makeGroups, defaultOrder: { by: 'name', direction: 'asc' } })),
|
|
111
|
+
React.createElement("section", null,
|
|
112
|
+
React.createElement("h3", null, "Loading"),
|
|
113
|
+
React.createElement(ExampleTable, { title: "Loading state", isLoading: true }))))
|
|
104
114
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { TableVirtuosoHandle, TableVirtuosoProps } from 'react-virtuoso';
|
|
3
|
-
import type
|
|
3
|
+
import { type Column, type OrderDirection, type Row, type RowContentProps, type TableContext } from './types';
|
|
4
4
|
export type { Column as VirtualizedTableColumn, Row as VirtualizedTableRow } from './types';
|
|
5
5
|
export interface VirtualizedTableProps extends Omit<TableVirtuosoProps<Row, TableContext>, 'data' | 'rows' | 'context' | 'components' | 'itemContent' | 'fixedHeaderContent'> {
|
|
6
6
|
/** Rows to display in the table */
|
|
@@ -38,6 +38,10 @@ export interface VirtualizedTableProps extends Omit<TableVirtuosoProps<Row, Tabl
|
|
|
38
38
|
order: OrderDirection;
|
|
39
39
|
orderBy: string;
|
|
40
40
|
}) => void;
|
|
41
|
+
/** Is the table loading data */
|
|
42
|
+
isLoading?: boolean;
|
|
43
|
+
/** The number of skeletons to show when loading */
|
|
44
|
+
skeletonCount?: number;
|
|
41
45
|
}
|
|
42
46
|
export declare const VirtualizedTable: React.ForwardRefExoticComponent<VirtualizedTableProps & React.RefAttributes<TableVirtuosoHandle>>;
|
|
43
47
|
export default VirtualizedTable;
|
|
@@ -4,17 +4,24 @@ import { GroupedTableVirtuoso, TableVirtuoso } from 'react-virtuoso';
|
|
|
4
4
|
import { FixedHeaderContent } from './FixedHeaderContent';
|
|
5
5
|
import { RowContent } from './RowContent';
|
|
6
6
|
import { getComparator, stableSort } from './helpers';
|
|
7
|
+
import { _isLoading } from './types';
|
|
7
8
|
import { virtuosoComponents } from './virtuosoComponents';
|
|
8
|
-
export const VirtualizedTable = forwardRef(({ rows, columns, groups, defaultOrder, secondarySort, selectedItems = [], isSelectedItem = () => false, isNewItem, context, componentsProps, components = virtuosoComponents, onSortChange, ...props }, ref) => {
|
|
9
|
+
export const VirtualizedTable = forwardRef(({ rows, columns, groups, defaultOrder, secondarySort, selectedItems = [], isSelectedItem = () => false, isNewItem, context, componentsProps, components = virtuosoComponents, onSortChange, isLoading, skeletonCount = 10, ...props }, ref) => {
|
|
9
10
|
const [orderDirection, setOrderDirection] = useState(defaultOrder?.direction ?? 'asc');
|
|
10
11
|
const [orderBy, setOrderBy] = useState(defaultOrder?.by);
|
|
11
12
|
const data = useMemo(() => {
|
|
13
|
+
if (isLoading) {
|
|
14
|
+
return Array.from({ length: skeletonCount }).map((_, i) => ({
|
|
15
|
+
[_isLoading]: true,
|
|
16
|
+
id: `skeleton-${i}`
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
12
19
|
const sortedRows = orderBy
|
|
13
20
|
? stableSort(rows, getComparator(orderDirection, orderBy))
|
|
14
21
|
: rows;
|
|
15
22
|
return secondarySort ? secondarySort(sortedRows) : sortedRows;
|
|
16
|
-
}, [rows, orderBy, orderDirection, secondarySort]);
|
|
17
|
-
const { groupLabels, groupCounts } = groups?.(data) ?? {};
|
|
23
|
+
}, [rows, orderBy, orderDirection, secondarySort, isLoading, skeletonCount]);
|
|
24
|
+
const { groupLabels, groupCounts } = (isLoading ? undefined : groups?.(data)) ?? {};
|
|
18
25
|
const tableContext = {
|
|
19
26
|
...context,
|
|
20
27
|
data,
|
|
@@ -33,8 +40,12 @@ export const VirtualizedTable = forwardRef(({ rows, columns, groups, defaultOrde
|
|
|
33
40
|
...props,
|
|
34
41
|
context: tableContext,
|
|
35
42
|
components,
|
|
36
|
-
fixedHeaderContent: () => (React.createElement(FixedHeaderContent, { columns: columns, orderDirection: orderDirection, orderBy: orderBy, onClick: handleSort })),
|
|
37
|
-
itemContent: (index) =>
|
|
43
|
+
fixedHeaderContent: () => (React.createElement(FixedHeaderContent, { columns: columns, orderDirection: orderDirection, orderBy: orderBy, onClick: handleSort, isLoading: isLoading })),
|
|
44
|
+
itemContent: (index, ...args) => {
|
|
45
|
+
const ctx = args[args.length - 1];
|
|
46
|
+
const freshData = ctx?.data ?? data;
|
|
47
|
+
return (React.createElement(RowContent, { ...componentsProps?.rowContent, row: freshData[index], columns: columns }));
|
|
48
|
+
}
|
|
38
49
|
};
|
|
39
50
|
return groupCounts ? (React.createElement(GroupedTableVirtuoso, { ...shared,
|
|
40
51
|
// both handles expose the same scroll methods, only the location typing differs
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { TableCellProps } from '@mui/material';
|
|
2
2
|
import type React from 'react';
|
|
3
|
-
export type Row = Record<string, unknown>;
|
|
3
|
+
export type Row = Record<string | symbol, unknown>;
|
|
4
4
|
export type OrderDirection = 'asc' | 'desc';
|
|
5
|
+
export declare const _isLoading: unique symbol;
|
|
5
6
|
export interface Column {
|
|
6
7
|
id: string;
|
|
7
8
|
label?: React.ReactNode;
|
|
@@ -12,6 +13,7 @@ export interface Column {
|
|
|
12
13
|
disablePadding?: boolean;
|
|
13
14
|
sortable?: boolean;
|
|
14
15
|
disableClick?: boolean;
|
|
16
|
+
skeleton?: React.ReactNode;
|
|
15
17
|
}
|
|
16
18
|
export interface TableContext {
|
|
17
19
|
data?: Row[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const _isLoading = Symbol('_isLoading');
|