@linagora/twake-mui 9.2.0 → 9.4.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 +16 -0
- package/dist/components/VirtualizedTable/Cell.d.ts +9 -0
- package/dist/components/VirtualizedTable/Cell.js +41 -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 +10 -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 +42 -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 +2 -0
- package/dist/index.js +1 -0
- package/dist/lib/overrides.js +76 -4
- package/dist/stories/Tooltip.stories.d.ts +1 -1
- package/dist/stories/Tooltip.stories.js +17 -10
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
# @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)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **twake-mui:** Install react-virtuoso ([4248221](https://github.com/linagora/twake-ui/commit/424822190001b87a38679957ce042322078aafd0))
|
|
7
|
+
* **twake-mui:** Install rooks ([c1d4d9a](https://github.com/linagora/twake-ui/commit/c1d4d9aa5ea535f070be5b75dec9cb864103a547))
|
|
8
|
+
* **twake-mui:** Migrate VirtualizedTable from cozy-ui ([2d3a3cf](https://github.com/linagora/twake-ui/commit/2d3a3cf6751435d247e7c14388010f1450cd9c96))
|
|
9
|
+
|
|
10
|
+
# @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)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **twake-mui:** Migrate Tooltip from cozy-ui ([6a5263a](https://github.com/linagora/twake-ui/commit/6a5263a2d5246798a1d003a8a2a7e0b5183bbe0d))
|
|
16
|
+
|
|
1
17
|
# @linagora/twake-mui [9.2.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@9.1.0...@linagora/twake-mui@9.2.0) (2026-09-22)
|
|
2
18
|
|
|
3
19
|
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
if (column.disableClick)
|
|
15
|
+
return;
|
|
16
|
+
isLongPress.current = true;
|
|
17
|
+
onLongPress?.(row, column);
|
|
18
|
+
}, { duration: LONG_PRESS_DURATION });
|
|
19
|
+
const handleClick = () => {
|
|
20
|
+
if (column.disableClick)
|
|
21
|
+
return;
|
|
22
|
+
if (isLongPress.current) {
|
|
23
|
+
isLongPress.current = false;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const isDoubleClick = now - lastClickTime.current < DOUBLE_CLICK_DELAY;
|
|
28
|
+
lastClickTime.current = now;
|
|
29
|
+
if (!isDoubleClick)
|
|
30
|
+
onClick?.(row, column);
|
|
31
|
+
};
|
|
32
|
+
return (React.createElement(TableCell, { ref: longPressRef, sx: {
|
|
33
|
+
cursor: onClick ? 'pointer' : undefined,
|
|
34
|
+
width: column.width,
|
|
35
|
+
maxWidth: column.maxWidth
|
|
36
|
+
}, 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
|
|
37
|
+
? React.Children.map(children, child => React.isValidElement(child)
|
|
38
|
+
? React.cloneElement(child, { row, columns, column, cell })
|
|
39
|
+
: null)
|
|
40
|
+
: cell));
|
|
41
|
+
};
|
|
@@ -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,10 @@
|
|
|
1
|
+
import { TableCell, TableSortLabel } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export const HeadCell = ({ column, orderBy, orderDirection, onClick }) => {
|
|
4
|
+
const isActive = orderBy === column.id;
|
|
5
|
+
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 },
|
|
6
|
+
column.label,
|
|
7
|
+
isActive && (React.createElement("span", { className: "u-visuallyhidden" }, orderDirection === 'desc'
|
|
8
|
+
? 'sorted descending'
|
|
9
|
+
: 'sorted ascending')))) : (column.label)));
|
|
10
|
+
};
|
|
@@ -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,42 @@
|
|
|
1
|
+
import { TableCell } from '@mui/material';
|
|
2
|
+
import React, { forwardRef, 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 sortedRows = orderBy
|
|
12
|
+
? stableSort(rows, getComparator(orderDirection, orderBy))
|
|
13
|
+
: rows;
|
|
14
|
+
const data = secondarySort ? secondarySort(sortedRows) : sortedRows;
|
|
15
|
+
const { groupLabels, groupCounts } = groups?.(data) ?? {};
|
|
16
|
+
const tableContext = {
|
|
17
|
+
...context,
|
|
18
|
+
data,
|
|
19
|
+
selectedItems,
|
|
20
|
+
isSelectedItem,
|
|
21
|
+
isNewItem
|
|
22
|
+
};
|
|
23
|
+
const handleSort = (property) => {
|
|
24
|
+
const isAsc = orderBy === property && orderDirection === 'asc';
|
|
25
|
+
const newOrder = isAsc ? 'desc' : 'asc';
|
|
26
|
+
setOrderDirection(newOrder);
|
|
27
|
+
setOrderBy(property);
|
|
28
|
+
onSortChange?.({ order: newOrder, orderBy: property });
|
|
29
|
+
};
|
|
30
|
+
const shared = {
|
|
31
|
+
...props,
|
|
32
|
+
context: tableContext,
|
|
33
|
+
components,
|
|
34
|
+
fixedHeaderContent: () => (React.createElement(FixedHeaderContent, { columns: columns, orderDirection: orderDirection, orderBy: orderBy, onClick: handleSort })),
|
|
35
|
+
itemContent: (index) => (React.createElement(RowContent, { ...componentsProps?.rowContent, row: data[index], columns: columns }))
|
|
36
|
+
};
|
|
37
|
+
return groupCounts ? (React.createElement(GroupedTableVirtuoso, { ...shared,
|
|
38
|
+
// both handles expose the same scroll methods, only the location typing differs
|
|
39
|
+
ref: ref, groupCounts: groupCounts, groupContent: index => (React.createElement(TableCell, { colSpan: columns.length, size: "small" }, groupLabels?.[index])) })) : (React.createElement(TableVirtuoso, { ...shared, ref: ref, data: data }));
|
|
40
|
+
});
|
|
41
|
+
VirtualizedTable.displayName = 'VirtualizedTable';
|
|
42
|
+
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,7 @@ 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';
|
|
26
27
|
export { useBreakpoints } from './hooks/useBreakpoints';
|
|
27
28
|
export type { TwakeTheme } from './lib/theme';
|
|
28
29
|
export type { PaletteJson } from './lib/types';
|
|
@@ -41,4 +42,5 @@ export type { NavIconProps } from './components/NavIcon';
|
|
|
41
42
|
export type { NavTextProps } from './components/NavText';
|
|
42
43
|
export type { NavDesktopLimiterProps } from './components/NavDesktopLimiter';
|
|
43
44
|
export type { NavDesktopDropdownProps } from './components/NavDesktopDropdown';
|
|
45
|
+
export type { VirtualizedTableProps, VirtualizedTableColumn, VirtualizedTableRow } from './components/VirtualizedTable';
|
|
44
46
|
export type { Breakpoints } from './hooks/useBreakpoints';
|
package/dist/index.js
CHANGED
|
@@ -26,4 +26,5 @@ 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';
|
|
29
30
|
export { useBreakpoints } from './hooks/useBreakpoints';
|
package/dist/lib/overrides.js
CHANGED
|
@@ -1000,10 +1000,17 @@ export const overrides = {
|
|
|
1000
1000
|
styleOverrides: {
|
|
1001
1001
|
tooltip: ({ theme }) => ({
|
|
1002
1002
|
backgroundColor: alpha(theme.palette.grey[800], 0.9),
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1003
|
+
borderRadius: radius.sm,
|
|
1004
|
+
variants: [
|
|
1005
|
+
{
|
|
1006
|
+
props: ({ ownerState }) => !ownerState.touch,
|
|
1007
|
+
style: {
|
|
1008
|
+
fontSize: theme.typography.pxToRem(16),
|
|
1009
|
+
lineHeight: 1.3,
|
|
1010
|
+
padding: '8px 12px'
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
]
|
|
1007
1014
|
})
|
|
1008
1015
|
}
|
|
1009
1016
|
},
|
|
@@ -1023,5 +1030,70 @@ export const overrides = {
|
|
|
1023
1030
|
}
|
|
1024
1031
|
})
|
|
1025
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
|
+
}
|
|
1026
1098
|
}
|
|
1027
1099
|
};
|
|
@@ -3,5 +3,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
|
3
3
|
declare const meta: Meta<typeof Tooltip>;
|
|
4
4
|
export default meta;
|
|
5
5
|
type Story = StoryObj<typeof Tooltip>;
|
|
6
|
-
export declare const
|
|
6
|
+
export declare const Default: Story;
|
|
7
7
|
export declare const Screenshot: Story;
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CarbonCopy, Icon } from '@linagora/twake-icons';
|
|
2
|
+
import { Box, Stack, Tooltip, Typography } from '@mui/material';
|
|
2
3
|
import React from 'react';
|
|
4
|
+
const title = (React.createElement(React.Fragment, null,
|
|
5
|
+
React.createElement(Stack, { direction: "row", spacing: 1, sx: { alignItems: 'center', mb: 1 } },
|
|
6
|
+
React.createElement(Icon, { icon: CarbonCopy }),
|
|
7
|
+
React.createElement(Typography, { variant: "body1", color: "inherit" }, "Carbon copy")),
|
|
8
|
+
React.createElement(Typography, { variant: "caption", color: "inherit" }, "Indicates whether the document is defined as \u201Cauthentic and original\u201D by Cozy Cloud, the host of your Cozy, as it can claim that it comes directly from a third-party service, without having undergone any modification.")));
|
|
3
9
|
const meta = {
|
|
4
10
|
title: 'Tooltip',
|
|
5
11
|
component: Tooltip,
|
|
6
12
|
parameters: { layout: 'centered' },
|
|
7
|
-
tags: ['autodocs']
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
decorators: [
|
|
15
|
+
// The dark tooltip shares the dark paper colour, so it would vanish on it
|
|
16
|
+
(Story) => (React.createElement(Box, { sx: theme => theme.applyStyles('dark', { bgcolor: 'background.default' }) },
|
|
17
|
+
React.createElement(Story, null)))
|
|
18
|
+
]
|
|
8
19
|
};
|
|
9
20
|
export default meta;
|
|
10
|
-
export const
|
|
21
|
+
export const Default = {
|
|
11
22
|
argTypes: {
|
|
12
23
|
placement: {
|
|
13
24
|
control: 'select',
|
|
@@ -25,17 +36,13 @@ export const Docs = {
|
|
|
25
36
|
arrow: { control: 'boolean' }
|
|
26
37
|
},
|
|
27
38
|
args: {
|
|
28
|
-
title
|
|
29
|
-
React.createElement(Typography, { variant: "body1", color: "inherit" }, "Carbon copy"),
|
|
30
|
-
React.createElement(Typography, { variant: "caption", color: "inherit" }, "Indicates whether the document is defined as \u201Cauthentic and original\u201D"))),
|
|
39
|
+
title,
|
|
31
40
|
children: React.createElement("u", null, "hover over me")
|
|
32
41
|
}
|
|
33
42
|
};
|
|
34
43
|
export const Screenshot = {
|
|
35
44
|
tags: ['argos'],
|
|
36
|
-
render: () => (React.createElement("div", { style: { padding:
|
|
37
|
-
React.createElement(Tooltip, { open: true, title:
|
|
38
|
-
React.createElement(Typography, { variant: "body1", color: "inherit" }, "Carbon copy"),
|
|
39
|
-
React.createElement(Typography, { variant: "caption", color: "inherit" }, "Indicates whether the document is defined as \u201Cauthentic and original\u201D")) },
|
|
45
|
+
render: () => (React.createElement("div", { style: { padding: '48px 96px 200px' } },
|
|
46
|
+
React.createElement(Tooltip, { open: true, title: title },
|
|
40
47
|
React.createElement("u", null, "hover over me"))))
|
|
41
48
|
};
|
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.4.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",
|