@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 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,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { Column, Row, RowContentProps } from './types';
2
+ import { type Column, type Row, type RowContentProps } from './types';
3
3
  interface CellProps extends RowContentProps {
4
4
  row: Row;
5
5
  columns: Column[];
@@ -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 ? 'pointer' : undefined,
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: () => 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));
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
  };
@@ -5,6 +5,7 @@ interface FixedHeaderContentProps {
5
5
  orderBy?: string;
6
6
  orderDirection: OrderDirection;
7
7
  onClick: (columnId: string) => void;
8
+ isLoading?: boolean;
8
9
  }
9
10
  export declare const FixedHeaderContent: React.FC<FixedHeaderContentProps>;
10
11
  export {};
@@ -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 })))));
@@ -5,6 +5,7 @@ interface HeadCellProps {
5
5
  orderBy?: string;
6
6
  orderDirection: OrderDirection;
7
7
  onClick: () => void;
8
+ isLoading?: boolean;
8
9
  }
9
10
  export declare const HeadCell: React.FC<HeadCellProps>;
10
11
  export {};
@@ -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
- column.label,
20
+ content,
18
21
  isActive && (React.createElement("span", { className: "u-visuallyhidden" }, orderDirection === 'desc'
19
22
  ? t('VirtualizedTable.sortedDesc')
20
- : t('VirtualizedTable.sortedAsc'))))) : (column.label)));
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 { Column, OrderDirection, Row, RowContentProps, TableContext } from './types';
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) => (React.createElement(RowContent, { ...componentsProps?.rowContent, row: data[index], columns: columns }))
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');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linagora/twake-mui",
3
3
  "type": "module",
4
- "version": "10.2.0",
4
+ "version": "10.3.0",
5
5
  "description": "MUI v9 theme system for Twake applications",
6
6
  "author": "Twake",
7
7
  "license": "MIT",