@linagora/twake-mui 9.3.0 → 9.5.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 +24 -0
- package/dist/components/VirtualizedTable/Cell.d.ts +9 -0
- package/dist/components/VirtualizedTable/Cell.js +42 -0
- package/dist/components/VirtualizedTable/FixedHeaderContent.d.ts +10 -0
- package/dist/components/VirtualizedTable/FixedHeaderContent.js +4 -0
- package/dist/components/VirtualizedTable/HeadCell.d.ts +10 -0
- package/dist/components/VirtualizedTable/HeadCell.js +18 -0
- package/dist/components/VirtualizedTable/RowContent.d.ts +8 -0
- package/dist/components/VirtualizedTable/RowContent.js +4 -0
- package/dist/components/VirtualizedTable/TableRow.d.ts +6 -0
- package/dist/components/VirtualizedTable/TableRow.js +9 -0
- package/dist/components/VirtualizedTable/VirtualizedTable.stories.d.ts +7 -0
- package/dist/components/VirtualizedTable/VirtualizedTable.stories.js +104 -0
- package/dist/components/VirtualizedTable/helpers.d.ts +4 -0
- package/dist/components/VirtualizedTable/helpers.js +34 -0
- package/dist/components/VirtualizedTable/index.d.ts +43 -0
- package/dist/components/VirtualizedTable/index.js +44 -0
- package/dist/components/VirtualizedTable/locales/en.json +6 -0
- package/dist/components/VirtualizedTable/locales/fr.json +6 -0
- package/dist/components/VirtualizedTable/locales/ru.json +6 -0
- package/dist/components/VirtualizedTable/locales/vi.json +6 -0
- package/dist/components/VirtualizedTable/types.d.ts +35 -0
- package/dist/components/VirtualizedTable/types.js +1 -0
- package/dist/components/VirtualizedTable/virtuosoComponents.d.ts +7 -0
- package/dist/components/VirtualizedTable/virtuosoComponents.js +20 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lib/overrides.js +65 -0
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
# @linagora/twake-mui [9.5.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@9.4.0...@linagora/twake-mui@9.5.0) (2026-09-24)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **twake-mui:** Keep slow clicks on VirtualizedTable cells without a long press handler ([9e2d7a7](https://github.com/linagora/twake-ui/commit/9e2d7a786e911a509ba35b5ddedcacb43e9d2061))
|
|
7
|
+
* **twake-mui:** Show a pointer only on clickable VirtualizedTable cells ([eb98bc0](https://github.com/linagora/twake-ui/commit/eb98bc08892c5d6c162443cda249f81f57422512))
|
|
8
|
+
* **twake-mui:** Sort VirtualizedTable rows only when rows or order change ([1cb782a](https://github.com/linagora/twake-ui/commit/1cb782a1b7fa39c4bf12a814cb1aa5d6d1ae7cc0))
|
|
9
|
+
* **twake-mui:** Translate the VirtualizedTable sort direction announcement ([ffd5cc2](https://github.com/linagora/twake-ui/commit/ffd5cc2efaa437cc47d757b9d423e6648ddf6fe4))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **twake-mui:** Export VirtuosoMockContext to test VirtualizedTable ([ab456e5](https://github.com/linagora/twake-ui/commit/ab456e56fa0c121c67561e944d91d3c2641b98de))
|
|
15
|
+
|
|
16
|
+
# @linagora/twake-mui [9.4.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@9.3.0...@linagora/twake-mui@9.4.0) (2026-09-23)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **twake-mui:** Install react-virtuoso ([4248221](https://github.com/linagora/twake-ui/commit/424822190001b87a38679957ce042322078aafd0))
|
|
22
|
+
* **twake-mui:** Install rooks ([c1d4d9a](https://github.com/linagora/twake-ui/commit/c1d4d9aa5ea535f070be5b75dec9cb864103a547))
|
|
23
|
+
* **twake-mui:** Migrate VirtualizedTable from cozy-ui ([2d3a3cf](https://github.com/linagora/twake-ui/commit/2d3a3cf6751435d247e7c14388010f1450cd9c96))
|
|
24
|
+
|
|
1
25
|
# @linagora/twake-mui [9.3.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@9.2.0...@linagora/twake-mui@9.3.0) (2026-09-23)
|
|
2
26
|
|
|
3
27
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TableCell } from '@mui/material';
|
|
2
|
+
import React, { useRef } from 'react';
|
|
3
|
+
import { useOnLongPress } from 'rooks';
|
|
4
|
+
import { getPath } from './helpers';
|
|
5
|
+
const DOUBLE_CLICK_DELAY = 400;
|
|
6
|
+
const LONG_PRESS_DURATION = 300;
|
|
7
|
+
export const Cell = ({ row, columns, column, onClick, onDoubleClick, onLongPress, children }) => {
|
|
8
|
+
// onClick is triggered after a long press anyway, so we need this flag to swallow it
|
|
9
|
+
const isLongPress = useRef(false);
|
|
10
|
+
const lastClickTime = useRef(0);
|
|
11
|
+
const cellContent = getPath(row, column.id);
|
|
12
|
+
const cell = cellContent === undefined ? '—' : cellContent;
|
|
13
|
+
const longPressRef = useOnLongPress(() => {
|
|
14
|
+
// without a long press handler, a slow click must stay a click
|
|
15
|
+
if (column.disableClick || !onLongPress)
|
|
16
|
+
return;
|
|
17
|
+
isLongPress.current = true;
|
|
18
|
+
onLongPress(row, column);
|
|
19
|
+
}, { duration: LONG_PRESS_DURATION });
|
|
20
|
+
const handleClick = () => {
|
|
21
|
+
if (column.disableClick)
|
|
22
|
+
return;
|
|
23
|
+
if (isLongPress.current) {
|
|
24
|
+
isLongPress.current = false;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const isDoubleClick = now - lastClickTime.current < DOUBLE_CLICK_DELAY;
|
|
29
|
+
lastClickTime.current = now;
|
|
30
|
+
if (!isDoubleClick)
|
|
31
|
+
onClick?.(row, column);
|
|
32
|
+
};
|
|
33
|
+
return (React.createElement(TableCell, { ref: longPressRef, sx: {
|
|
34
|
+
cursor: !!onClick && !column.disableClick ? 'pointer' : undefined,
|
|
35
|
+
width: column.width,
|
|
36
|
+
maxWidth: column.maxWidth
|
|
37
|
+
}, className: column.noWrap ? 'u-ellipsis' : undefined, align: column.textAlign ?? 'left', padding: column.disablePadding ? 'none' : 'normal', onClick: handleClick, onDoubleClick: () => column.disableClick ? undefined : onDoubleClick?.(row, column), onContextMenu: ev => isLongPress.current && ev.preventDefault() }, children
|
|
38
|
+
? React.Children.map(children, child => React.isValidElement(child)
|
|
39
|
+
? React.cloneElement(child, { row, columns, column, cell })
|
|
40
|
+
: null)
|
|
41
|
+
: cell));
|
|
42
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Column, OrderDirection } from './types';
|
|
3
|
+
interface FixedHeaderContentProps {
|
|
4
|
+
columns: Column[];
|
|
5
|
+
orderBy?: string;
|
|
6
|
+
orderDirection: OrderDirection;
|
|
7
|
+
onClick: (columnId: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const FixedHeaderContent: React.FC<FixedHeaderContentProps>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TableRow } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
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) })))));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Column, OrderDirection } from './types';
|
|
3
|
+
interface HeadCellProps {
|
|
4
|
+
column: Column;
|
|
5
|
+
orderBy?: string;
|
|
6
|
+
orderDirection: OrderDirection;
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const HeadCell: React.FC<HeadCellProps>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TableCell, TableSortLabel } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useExtendI18n, useI18n } from 'twake-i18n';
|
|
4
|
+
import en from './locales/en.json';
|
|
5
|
+
import fr from './locales/fr.json';
|
|
6
|
+
import ru from './locales/ru.json';
|
|
7
|
+
import vi from './locales/vi.json';
|
|
8
|
+
const locales = { en, fr, ru, vi };
|
|
9
|
+
export const HeadCell = ({ column, orderBy, orderDirection, onClick }) => {
|
|
10
|
+
useExtendI18n(locales);
|
|
11
|
+
const { t } = useI18n();
|
|
12
|
+
const isActive = orderBy === column.id;
|
|
13
|
+
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 },
|
|
14
|
+
column.label,
|
|
15
|
+
isActive && (React.createElement("span", { className: "u-visuallyhidden" }, orderDirection === 'desc'
|
|
16
|
+
? t('VirtualizedTable.sortedDesc')
|
|
17
|
+
: t('VirtualizedTable.sortedAsc'))))) : (column.label)));
|
|
18
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Column, Row, RowContentProps } from './types';
|
|
3
|
+
interface RowContentComponentProps extends RowContentProps {
|
|
4
|
+
row: Row;
|
|
5
|
+
columns: Column[];
|
|
6
|
+
}
|
|
7
|
+
export declare const RowContent: React.FC<RowContentComponentProps>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Cell } from './Cell';
|
|
3
|
+
export const RowContent = React.memo(({ row, columns, children, onClick, onDoubleClick, onLongPress }) => (React.createElement(React.Fragment, null, columns.map(column => (React.createElement(Cell, { key: column.id, row: row, columns: columns, column: column, onClick: onClick, onDoubleClick: onDoubleClick, onLongPress: onLongPress }, children))))));
|
|
4
|
+
RowContent.displayName = 'RowContent';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { TableComponents } from 'react-virtuoso';
|
|
3
|
+
import type { Row, TableContext } from './types';
|
|
4
|
+
type TableRowProps = React.ComponentProps<NonNullable<TableComponents<Row, TableContext>['TableRow']>>;
|
|
5
|
+
export declare const TableRow: React.FC<TableRowProps>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TableRow as MuiTableRow } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export const TableRow = ({ item, context, ...props }) => {
|
|
4
|
+
// grouped tables do not receive `item`, so we read it back from the sorted data
|
|
5
|
+
const row = item ?? context.data?.[props['data-item-index']];
|
|
6
|
+
const isSelected = row ? context.isSelectedItem(row) : false;
|
|
7
|
+
const isNew = row ? context.isNewItem?.(row) : false;
|
|
8
|
+
return (React.createElement(MuiTableRow, { ...props, className: isNew ? 'new' : undefined, selected: isSelected, hover: true }));
|
|
9
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { VirtualizedTable } from './index';
|
|
3
|
+
declare const meta: Meta<typeof VirtualizedTable>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof meta>;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const Screenshot: Story;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Button, Typography } from '@mui/material';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { fn } from 'storybook/test';
|
|
4
|
+
import { VirtualizedTable } from './index';
|
|
5
|
+
const createData = (id, name, calories, fat, carbs, protein) => ({ id, name, calories, fat, carbs, protein });
|
|
6
|
+
const rows = [
|
|
7
|
+
createData(0, 'Cupcake', 305, 3.7, 67, 4.3),
|
|
8
|
+
createData(1, 'Frozen yoghurt (unselectable)', 159, 6.0, 24, 4.0),
|
|
9
|
+
createData(2, 'Donut', 452, 25.0, 51, 4.9),
|
|
10
|
+
createData(3, 'Eclair', 262, 16.0, 24, 6.0),
|
|
11
|
+
createData(4, 'Ice cream sandwich', 237, 9.0, 37, 4.3),
|
|
12
|
+
createData(5, 'Gingerbread', 356, 16.0, 49, 3.9),
|
|
13
|
+
createData(6, 'Honeycomb', 408, 3.2, 87, 6.5),
|
|
14
|
+
createData(7, 'Jelly Bean', 375, 0.0, 94, 0.0),
|
|
15
|
+
createData(8, 'KitKat', 518, 26.0, 65, 7.0),
|
|
16
|
+
createData(9, 'Oreo', 437, 18.0, 63, 4.0),
|
|
17
|
+
createData(10, 'Marshmallow', 318, 0, 81, 2.0),
|
|
18
|
+
createData(11, 'Lollipop', 392, 0.2, 98, 0.0),
|
|
19
|
+
createData(12, 'Nougat', 360, 19.0, 9, 37.0),
|
|
20
|
+
createData(13, 'Ice cream with a very long list of ingredient to see how the table can handle this kind of item, and this is the end', 237, 9.0, 37, 4.3)
|
|
21
|
+
];
|
|
22
|
+
const columns = [
|
|
23
|
+
{ id: 'name', label: 'Dessert', width: 300, maxWidth: 300, noWrap: true },
|
|
24
|
+
{
|
|
25
|
+
id: 'calories',
|
|
26
|
+
label: 'Calories',
|
|
27
|
+
width: 80,
|
|
28
|
+
textAlign: 'left',
|
|
29
|
+
sortable: false
|
|
30
|
+
},
|
|
31
|
+
{ id: 'fat', label: 'Fat (g)', width: 85, textAlign: 'right' },
|
|
32
|
+
{ id: 'carbs', label: 'Carbs (g)', width: 115, textAlign: 'right' },
|
|
33
|
+
{
|
|
34
|
+
id: 'protein',
|
|
35
|
+
label: 'Protein (g)',
|
|
36
|
+
width: 115,
|
|
37
|
+
textAlign: 'right',
|
|
38
|
+
disableClick: true
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
// Very basic usage only works when Dessert is sorted "asc"
|
|
42
|
+
const makeGroups = () => ({
|
|
43
|
+
groupLabels: ['C', 'D', 'E', 'Others'],
|
|
44
|
+
groupCounts: [1, 1, 1, 11]
|
|
45
|
+
});
|
|
46
|
+
const UNSELECTABLE_ID = 1;
|
|
47
|
+
const onDoubleClick = fn().mockName('onDoubleClick');
|
|
48
|
+
const onLongPress = fn().mockName('onLongPress');
|
|
49
|
+
const ExampleTable = ({ title, ...props }) => {
|
|
50
|
+
const [selected, setSelected] = useState([]);
|
|
51
|
+
const toggle = (id) => setSelected(ids => ids.includes(id) ? ids.filter(i => i !== id) : [...ids, id]);
|
|
52
|
+
const toggleAll = () => setSelected(ids => ids.length ? [] : rows.map(r => r.id).filter(id => id !== 1));
|
|
53
|
+
return (React.createElement(React.Fragment, null,
|
|
54
|
+
React.createElement(Typography, { sx: { mt: 2 }, variant: "h4" }, title),
|
|
55
|
+
React.createElement(Button, { sx: { my: 2 }, variant: "text", onClick: toggleAll }, "Select all"),
|
|
56
|
+
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: {
|
|
58
|
+
rowContent: {
|
|
59
|
+
onClick: row => row.id !== UNSELECTABLE_ID && toggle(row.id),
|
|
60
|
+
onDoubleClick: (row, column) => row.id !== UNSELECTABLE_ID && onDoubleClick(row, column),
|
|
61
|
+
onLongPress: (row, column) => row.id !== UNSELECTABLE_ID && onLongPress(row, column)
|
|
62
|
+
}
|
|
63
|
+
}, ...props }))));
|
|
64
|
+
};
|
|
65
|
+
const meta = {
|
|
66
|
+
title: 'VirtualizedTable',
|
|
67
|
+
component: VirtualizedTable,
|
|
68
|
+
tags: ['autodocs'],
|
|
69
|
+
argTypes: {
|
|
70
|
+
rows: { control: 'object' },
|
|
71
|
+
columns: { control: 'object' },
|
|
72
|
+
defaultOrder: { control: 'object' },
|
|
73
|
+
selectedItems: { control: 'object' },
|
|
74
|
+
groups: { control: false },
|
|
75
|
+
secondarySort: { control: false },
|
|
76
|
+
isSelectedItem: { control: false },
|
|
77
|
+
isNewItem: { control: false },
|
|
78
|
+
context: { control: false },
|
|
79
|
+
componentsProps: { control: false },
|
|
80
|
+
components: { control: false }
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
export default meta;
|
|
84
|
+
export const Default = {
|
|
85
|
+
args: {
|
|
86
|
+
rows,
|
|
87
|
+
columns,
|
|
88
|
+
defaultOrder: { by: 'name', direction: 'asc' }
|
|
89
|
+
},
|
|
90
|
+
render: args => React.createElement(ExampleTable, { title: "Table", ...args })
|
|
91
|
+
};
|
|
92
|
+
// Visual Regression - Combined view for Argos testing
|
|
93
|
+
export const Screenshot = {
|
|
94
|
+
tags: ['argos'],
|
|
95
|
+
render: () => (React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: '32px' } },
|
|
96
|
+
React.createElement("section", null,
|
|
97
|
+
React.createElement("h3", null, "Flat"),
|
|
98
|
+
React.createElement(ExampleTable, { title: "Not sorted table" }),
|
|
99
|
+
React.createElement(ExampleTable, { title: "Sorted table", defaultOrder: { by: 'name', direction: 'asc' } })),
|
|
100
|
+
React.createElement("section", null,
|
|
101
|
+
React.createElement("h3", null, "Grouped"),
|
|
102
|
+
React.createElement(ExampleTable, { title: "Not sorted table", groups: makeGroups }),
|
|
103
|
+
React.createElement(ExampleTable, { title: "Sorted table", groups: makeGroups, defaultOrder: { by: 'name', direction: 'asc' } }))))
|
|
104
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OrderDirection, Row } from './types';
|
|
2
|
+
export declare const getPath: (row: Row, path: string) => unknown;
|
|
3
|
+
export declare const getComparator: (orderDirection: OrderDirection, orderBy: string, lang?: string) => ((a: Row, b: Row) => number);
|
|
4
|
+
export declare const stableSort: (rows: Row[], comparator: (a: Row, b: Row) => number) => Row[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const getPath = (row, path) => path
|
|
2
|
+
.split('.')
|
|
3
|
+
.reduce((value, key) => value !== null && typeof value === 'object'
|
|
4
|
+
? value[key]
|
|
5
|
+
: undefined, row);
|
|
6
|
+
const ISO_DATE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}/;
|
|
7
|
+
const descendingComparator = (a, b, orderDirection, orderBy, lang) => {
|
|
8
|
+
const aValue = getPath(a, orderBy) ?? '';
|
|
9
|
+
const bValue = getPath(b, orderBy) ?? '';
|
|
10
|
+
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
11
|
+
if (ISO_DATE.test(aValue)) {
|
|
12
|
+
return new Date(bValue).getTime() - new Date(aValue).getTime();
|
|
13
|
+
}
|
|
14
|
+
if (!isNaN(parseInt(aValue))) {
|
|
15
|
+
return parseInt(bValue) - parseInt(aValue);
|
|
16
|
+
}
|
|
17
|
+
const collator = new Intl.Collator(lang || 'en', {
|
|
18
|
+
caseFirst: orderDirection === 'asc' ? 'upper' : 'lower'
|
|
19
|
+
});
|
|
20
|
+
return collator.compare(bValue, aValue);
|
|
21
|
+
}
|
|
22
|
+
return Number(bValue) - Number(aValue);
|
|
23
|
+
};
|
|
24
|
+
export const getComparator = (orderDirection, orderBy, lang) => {
|
|
25
|
+
return orderDirection === 'desc'
|
|
26
|
+
? (a, b) => descendingComparator(a, b, orderDirection, orderBy, lang)
|
|
27
|
+
: (a, b) => -descendingComparator(a, b, orderDirection, orderBy, lang);
|
|
28
|
+
};
|
|
29
|
+
export const stableSort = (rows, comparator) => {
|
|
30
|
+
return rows
|
|
31
|
+
.map((row, index) => [row, index])
|
|
32
|
+
.sort((a, b) => comparator(a[0], b[0]) || a[1] - b[1])
|
|
33
|
+
.map(([row]) => row);
|
|
34
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TableVirtuosoHandle, TableVirtuosoProps } from 'react-virtuoso';
|
|
3
|
+
import type { Column, OrderDirection, Row, RowContentProps, TableContext } from './types';
|
|
4
|
+
export type { Column as VirtualizedTableColumn, Row as VirtualizedTableRow } from './types';
|
|
5
|
+
export interface VirtualizedTableProps extends Omit<TableVirtuosoProps<Row, TableContext>, 'data' | 'rows' | 'context' | 'components' | 'itemContent' | 'fixedHeaderContent'> {
|
|
6
|
+
/** Rows to display in the table */
|
|
7
|
+
rows: Row[];
|
|
8
|
+
/** Column configuration */
|
|
9
|
+
columns: Column[];
|
|
10
|
+
/** Split the sorted rows into groups */
|
|
11
|
+
groups?: (rows: Row[]) => {
|
|
12
|
+
groupLabels: React.ReactNode[];
|
|
13
|
+
groupCounts: number[];
|
|
14
|
+
};
|
|
15
|
+
/** Default sorting configuration */
|
|
16
|
+
defaultOrder?: {
|
|
17
|
+
direction?: OrderDirection;
|
|
18
|
+
by?: string;
|
|
19
|
+
};
|
|
20
|
+
/** Applied after the column sort, e.g. to keep directories before files */
|
|
21
|
+
secondarySort?: (rows: Row[]) => Row[];
|
|
22
|
+
/** Array of selected items */
|
|
23
|
+
selectedItems?: unknown[];
|
|
24
|
+
/** Function to determine if a row is selected */
|
|
25
|
+
isSelectedItem?: (row: Row) => boolean;
|
|
26
|
+
/** Function to determine if a row is new */
|
|
27
|
+
isNewItem?: (row: Row) => boolean;
|
|
28
|
+
/** Context object passed down to children and virtuoso */
|
|
29
|
+
context?: Record<string, unknown>;
|
|
30
|
+
/** Props passed to child components */
|
|
31
|
+
componentsProps?: {
|
|
32
|
+
rowContent?: RowContentProps;
|
|
33
|
+
};
|
|
34
|
+
/** Custom components used by react-virtuoso */
|
|
35
|
+
components?: TableVirtuosoProps<Row, TableContext>['components'];
|
|
36
|
+
/** Callback called after the sort */
|
|
37
|
+
onSortChange?: (sort: {
|
|
38
|
+
order: OrderDirection;
|
|
39
|
+
orderBy: string;
|
|
40
|
+
}) => void;
|
|
41
|
+
}
|
|
42
|
+
export declare const VirtualizedTable: React.ForwardRefExoticComponent<VirtualizedTableProps & React.RefAttributes<TableVirtuosoHandle>>;
|
|
43
|
+
export default VirtualizedTable;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { TableCell } from '@mui/material';
|
|
2
|
+
import React, { forwardRef, useMemo, useState } from 'react';
|
|
3
|
+
import { GroupedTableVirtuoso, TableVirtuoso } from 'react-virtuoso';
|
|
4
|
+
import { FixedHeaderContent } from './FixedHeaderContent';
|
|
5
|
+
import { RowContent } from './RowContent';
|
|
6
|
+
import { getComparator, stableSort } from './helpers';
|
|
7
|
+
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
|
+
const [orderDirection, setOrderDirection] = useState(defaultOrder?.direction ?? 'asc');
|
|
10
|
+
const [orderBy, setOrderBy] = useState(defaultOrder?.by);
|
|
11
|
+
const data = useMemo(() => {
|
|
12
|
+
const sortedRows = orderBy
|
|
13
|
+
? stableSort(rows, getComparator(orderDirection, orderBy))
|
|
14
|
+
: rows;
|
|
15
|
+
return secondarySort ? secondarySort(sortedRows) : sortedRows;
|
|
16
|
+
}, [rows, orderBy, orderDirection, secondarySort]);
|
|
17
|
+
const { groupLabels, groupCounts } = groups?.(data) ?? {};
|
|
18
|
+
const tableContext = {
|
|
19
|
+
...context,
|
|
20
|
+
data,
|
|
21
|
+
selectedItems,
|
|
22
|
+
isSelectedItem,
|
|
23
|
+
isNewItem
|
|
24
|
+
};
|
|
25
|
+
const handleSort = (property) => {
|
|
26
|
+
const isAsc = orderBy === property && orderDirection === 'asc';
|
|
27
|
+
const newOrder = isAsc ? 'desc' : 'asc';
|
|
28
|
+
setOrderDirection(newOrder);
|
|
29
|
+
setOrderBy(property);
|
|
30
|
+
onSortChange?.({ order: newOrder, orderBy: property });
|
|
31
|
+
};
|
|
32
|
+
const shared = {
|
|
33
|
+
...props,
|
|
34
|
+
context: tableContext,
|
|
35
|
+
components,
|
|
36
|
+
fixedHeaderContent: () => (React.createElement(FixedHeaderContent, { columns: columns, orderDirection: orderDirection, orderBy: orderBy, onClick: handleSort })),
|
|
37
|
+
itemContent: (index) => (React.createElement(RowContent, { ...componentsProps?.rowContent, row: data[index], columns: columns }))
|
|
38
|
+
};
|
|
39
|
+
return groupCounts ? (React.createElement(GroupedTableVirtuoso, { ...shared,
|
|
40
|
+
// both handles expose the same scroll methods, only the location typing differs
|
|
41
|
+
ref: ref, groupCounts: groupCounts, groupContent: index => (React.createElement(TableCell, { colSpan: columns.length, size: "small" }, groupLabels?.[index])) })) : (React.createElement(TableVirtuoso, { ...shared, ref: ref, data: data }));
|
|
42
|
+
});
|
|
43
|
+
VirtualizedTable.displayName = 'VirtualizedTable';
|
|
44
|
+
export default VirtualizedTable;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { TableCellProps } from '@mui/material';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
export type Row = Record<string, unknown>;
|
|
4
|
+
export type OrderDirection = 'asc' | 'desc';
|
|
5
|
+
export interface Column {
|
|
6
|
+
id: string;
|
|
7
|
+
label?: React.ReactNode;
|
|
8
|
+
width?: number | string;
|
|
9
|
+
maxWidth?: number | string;
|
|
10
|
+
noWrap?: boolean;
|
|
11
|
+
textAlign?: TableCellProps['align'];
|
|
12
|
+
disablePadding?: boolean;
|
|
13
|
+
sortable?: boolean;
|
|
14
|
+
disableClick?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface TableContext {
|
|
17
|
+
data?: Row[];
|
|
18
|
+
selectedItems: unknown[];
|
|
19
|
+
isSelectedItem: (row: Row) => boolean;
|
|
20
|
+
isNewItem?: (row: Row) => boolean;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
export type CellHandler = (row: Row, column: Column) => void;
|
|
24
|
+
export interface CellChildProps {
|
|
25
|
+
row: Row;
|
|
26
|
+
columns: Column[];
|
|
27
|
+
column: Column;
|
|
28
|
+
cell: unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface RowContentProps {
|
|
31
|
+
children?: React.ReactNode;
|
|
32
|
+
onClick?: CellHandler;
|
|
33
|
+
onDoubleClick?: CellHandler;
|
|
34
|
+
onLongPress?: CellHandler;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TableComponents } from 'react-virtuoso';
|
|
2
|
+
import type { Row, TableContext } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* `context` is spread by virtuoso to every component. We destructure it so it
|
|
5
|
+
* never lands in the DOM.
|
|
6
|
+
*/
|
|
7
|
+
export declare const virtuosoComponents: TableComponents<Row, TableContext>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Paper, Table, TableBody, TableContainer, TableHead } from '@mui/material';
|
|
2
|
+
import React, { forwardRef } from 'react';
|
|
3
|
+
import { TableRow } from './TableRow';
|
|
4
|
+
/**
|
|
5
|
+
* `context` is spread by virtuoso to every component. We destructure it so it
|
|
6
|
+
* never lands in the DOM.
|
|
7
|
+
*/
|
|
8
|
+
export const virtuosoComponents = {
|
|
9
|
+
Scroller: forwardRef(function Scroller({ context, ...props }, ref) {
|
|
10
|
+
return (React.createElement(TableContainer, { ...props, ref: ref, component: Paper, elevation: 0, square: true }));
|
|
11
|
+
}),
|
|
12
|
+
Table: ({ context, ...props }) => React.createElement(Table, { ...props }),
|
|
13
|
+
TableHead: forwardRef(function TableHeadAdapter({ context, ...props }, ref) {
|
|
14
|
+
return React.createElement(TableHead, { ...props, ref: ref });
|
|
15
|
+
}),
|
|
16
|
+
TableBody: forwardRef(function TableBodyAdapter({ context, ...props }, ref) {
|
|
17
|
+
return React.createElement(TableBody, { ...props, ref: ref });
|
|
18
|
+
}),
|
|
19
|
+
TableRow
|
|
20
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export { NavDesktopLimiter } from './components/NavDesktopLimiter';
|
|
|
23
23
|
export { NavDesktopDropdown } from './components/NavDesktopDropdown';
|
|
24
24
|
export { nameToColor, supportedColors } from './components/Avatar/helpers';
|
|
25
25
|
export { default as AccordionExpandIcon } from './components/AccordionExpandIcon';
|
|
26
|
+
export { default as VirtualizedTable } from './components/VirtualizedTable';
|
|
27
|
+
export { VirtuosoMockContext } from 'react-virtuoso';
|
|
26
28
|
export { useBreakpoints } from './hooks/useBreakpoints';
|
|
27
29
|
export type { TwakeTheme } from './lib/theme';
|
|
28
30
|
export type { PaletteJson } from './lib/types';
|
|
@@ -41,4 +43,5 @@ export type { NavIconProps } from './components/NavIcon';
|
|
|
41
43
|
export type { NavTextProps } from './components/NavText';
|
|
42
44
|
export type { NavDesktopLimiterProps } from './components/NavDesktopLimiter';
|
|
43
45
|
export type { NavDesktopDropdownProps } from './components/NavDesktopDropdown';
|
|
46
|
+
export type { VirtualizedTableProps, VirtualizedTableColumn, VirtualizedTableRow } from './components/VirtualizedTable';
|
|
44
47
|
export type { Breakpoints } from './hooks/useBreakpoints';
|
package/dist/index.js
CHANGED
|
@@ -26,4 +26,6 @@ export { NavDesktopLimiter } from './components/NavDesktopLimiter';
|
|
|
26
26
|
export { NavDesktopDropdown } from './components/NavDesktopDropdown';
|
|
27
27
|
export { nameToColor, supportedColors } from './components/Avatar/helpers';
|
|
28
28
|
export { default as AccordionExpandIcon } from './components/AccordionExpandIcon';
|
|
29
|
+
export { default as VirtualizedTable } from './components/VirtualizedTable';
|
|
30
|
+
export { VirtuosoMockContext } from 'react-virtuoso';
|
|
29
31
|
export { useBreakpoints } from './hooks/useBreakpoints';
|
package/dist/lib/overrides.js
CHANGED
|
@@ -1030,5 +1030,70 @@ export const overrides = {
|
|
|
1030
1030
|
}
|
|
1031
1031
|
})
|
|
1032
1032
|
}
|
|
1033
|
+
},
|
|
1034
|
+
MuiTableHead: {
|
|
1035
|
+
styleOverrides: {
|
|
1036
|
+
root: ({ theme }) => ({
|
|
1037
|
+
backgroundColor: theme.vars.palette.background.paper
|
|
1038
|
+
})
|
|
1039
|
+
}
|
|
1040
|
+
},
|
|
1041
|
+
MuiTableRow: {
|
|
1042
|
+
styleOverrides: {
|
|
1043
|
+
root: ({ theme }) => ({
|
|
1044
|
+
'&.new': {
|
|
1045
|
+
backgroundColor: theme.vars.palette.action.hover
|
|
1046
|
+
},
|
|
1047
|
+
'&.disabled': {
|
|
1048
|
+
pointerEvents: 'none',
|
|
1049
|
+
opacity: 0.5
|
|
1050
|
+
}
|
|
1051
|
+
})
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
MuiTableCell: {
|
|
1055
|
+
styleOverrides: {
|
|
1056
|
+
root: {
|
|
1057
|
+
padding: '8px 4px'
|
|
1058
|
+
},
|
|
1059
|
+
head: ({ theme }) => ({
|
|
1060
|
+
...theme.typography.subtitle2,
|
|
1061
|
+
color: theme.vars.palette.text.secondary,
|
|
1062
|
+
lineHeight: 1.292
|
|
1063
|
+
}),
|
|
1064
|
+
body: ({ theme }) => ({
|
|
1065
|
+
color: theme.vars.palette.text.secondary,
|
|
1066
|
+
// We want cell's sizes to be 2rem of content plus padding and border (49px).
|
|
1067
|
+
// CssBaseline makes Twake apps border-box, where 2rem would be the total
|
|
1068
|
+
// height, so the cell opts back into content-box to keep the same row.
|
|
1069
|
+
boxSizing: 'content-box',
|
|
1070
|
+
height: '2rem'
|
|
1071
|
+
}),
|
|
1072
|
+
sizeSmall: ({ theme }) => ({
|
|
1073
|
+
...theme.typography.subtitle2,
|
|
1074
|
+
// restated because the root padding above beats MUI's own size variant
|
|
1075
|
+
padding: '0 16px',
|
|
1076
|
+
outline: `1px solid ${theme.vars.palette.background.default}`,
|
|
1077
|
+
borderBottom: 'none',
|
|
1078
|
+
backgroundColor: theme.vars.palette.background.default
|
|
1079
|
+
}),
|
|
1080
|
+
paddingCheckbox: {
|
|
1081
|
+
width: 32,
|
|
1082
|
+
padding: 0
|
|
1083
|
+
},
|
|
1084
|
+
stickyHeader: ({ theme }) => ({
|
|
1085
|
+
backgroundColor: theme.vars.palette.background.paper
|
|
1086
|
+
})
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
MuiTableSortLabel: {
|
|
1090
|
+
styleOverrides: {
|
|
1091
|
+
root: {
|
|
1092
|
+
padding: '8px 0'
|
|
1093
|
+
},
|
|
1094
|
+
icon: {
|
|
1095
|
+
fontSize: 14
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1033
1098
|
}
|
|
1034
1099
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linagora/twake-mui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.5.0",
|
|
5
5
|
"description": "MUI v9 theme system for Twake applications",
|
|
6
6
|
"author": "Twake",
|
|
7
7
|
"license": "MIT",
|
|
@@ -60,7 +60,9 @@
|
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@linagora/twake-css": "^1.0.0",
|
|
62
62
|
"@mui/utils": "^9.2.0",
|
|
63
|
-
"classnames": "^2.5.1"
|
|
63
|
+
"classnames": "^2.5.1",
|
|
64
|
+
"react-virtuoso": "4.13.0",
|
|
65
|
+
"rooks": "7.14.1"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
68
|
"@argos-ci/cli": "4.2.0",
|