cozy-ui 123.2.1 → 124.1.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/assets/icons/ui/cloud2.svg +1 -0
  3. package/assets/icons/ui/discuss.svg +1 -0
  4. package/assets/icons/ui/email-open.svg +1 -0
  5. package/assets/icons/ui/key2.svg +1 -0
  6. package/assets/icons/ui/peoples.svg +1 -0
  7. package/assets/icons/ui/security.svg +1 -0
  8. package/package.json +11 -6
  9. package/react/Filename/Readme.md +10 -8
  10. package/react/Filename/index.jsx +56 -10
  11. package/react/Filename/styles.styl +3 -0
  12. package/react/Icon/Readme.md +13 -1
  13. package/react/Icons/Cloud2.jsx +29 -0
  14. package/react/Icons/Discuss.jsx +16 -0
  15. package/react/Icons/EmailOpen.jsx +55 -0
  16. package/react/Icons/Key2.jsx +23 -0
  17. package/react/Icons/Peoples.jsx +12 -0
  18. package/react/Icons/Security.jsx +12 -0
  19. package/react/MuiCozyTheme/overrides/makeLightNormalOverrides.js +60 -0
  20. package/react/Table/Readme.md +80 -0
  21. package/react/Table/Virtualized/Cell.jsx +41 -0
  22. package/react/Table/Virtualized/Dnd/CustomDrag/CustomDragLayer.jsx +45 -0
  23. package/react/Table/Virtualized/Dnd/CustomDrag/DragPreview.jsx +43 -0
  24. package/react/Table/Virtualized/Dnd/CustomDrag/DragPreviewWrapper.jsx +52 -0
  25. package/react/Table/Virtualized/Dnd/DnDConfigWrapper.jsx +48 -0
  26. package/react/Table/Virtualized/Dnd/TableRow.jsx +86 -0
  27. package/react/Table/Virtualized/FixedHeaderContent.jsx +58 -0
  28. package/react/Table/Virtualized/HeadCell.jsx +45 -0
  29. package/react/Table/Virtualized/RowContent.jsx +35 -0
  30. package/react/Table/Virtualized/helpers.js +41 -0
  31. package/react/Table/Virtualized/helpers.spec.js +108 -0
  32. package/react/Table/Virtualized/index.jsx +104 -0
  33. package/react/Table/Virtualized/virtuosoComponents.jsx +61 -0
  34. package/react/TableRow/index.js +16 -1
  35. package/transpiled/react/Filename/index.d.ts +2 -1
  36. package/transpiled/react/Filename/index.js +49 -16
  37. package/transpiled/react/Icon/icons-sprite.d.ts +1 -1
  38. package/transpiled/react/Icon/icons-sprite.js +1 -1
  39. package/transpiled/react/Icons/Cloud2.d.ts +2 -0
  40. package/transpiled/react/Icons/Cloud2.js +26 -0
  41. package/transpiled/react/Icons/Discuss.d.ts +2 -0
  42. package/transpiled/react/Icons/Discuss.js +15 -0
  43. package/transpiled/react/Icons/EmailOpen.d.ts +2 -0
  44. package/transpiled/react/Icons/EmailOpen.js +53 -0
  45. package/transpiled/react/Icons/Key2.d.ts +2 -0
  46. package/transpiled/react/Icons/Key2.js +21 -0
  47. package/transpiled/react/Icons/Peoples.d.ts +2 -0
  48. package/transpiled/react/Icons/Peoples.js +13 -0
  49. package/transpiled/react/Icons/Security.d.ts +2 -0
  50. package/transpiled/react/Icons/Security.js +13 -0
  51. package/transpiled/react/MuiCozyTheme/overrides/makeDarkInvertedOverrides.d.ts +56 -0
  52. package/transpiled/react/MuiCozyTheme/overrides/makeDarkNormalOverrides.d.ts +56 -0
  53. package/transpiled/react/MuiCozyTheme/overrides/makeLightInvertedOverrides.d.ts +56 -0
  54. package/transpiled/react/MuiCozyTheme/overrides/makeLightNormalOverrides.d.ts +56 -0
  55. package/transpiled/react/MuiCozyTheme/overrides/makeLightNormalOverrides.js +59 -0
  56. package/transpiled/react/Table/Virtualized/Cell.d.ts +8 -0
  57. package/transpiled/react/Table/Virtualized/Cell.js +46 -0
  58. package/transpiled/react/Table/Virtualized/Dnd/CustomDrag/CustomDragLayer.d.ts +4 -0
  59. package/transpiled/react/Table/Virtualized/Dnd/CustomDrag/CustomDragLayer.js +47 -0
  60. package/transpiled/react/Table/Virtualized/Dnd/CustomDrag/DragPreview.d.ts +6 -0
  61. package/transpiled/react/Table/Virtualized/Dnd/CustomDrag/DragPreview.js +34 -0
  62. package/transpiled/react/Table/Virtualized/Dnd/CustomDrag/DragPreviewWrapper.d.ts +8 -0
  63. package/transpiled/react/Table/Virtualized/Dnd/CustomDrag/DragPreviewWrapper.js +63 -0
  64. package/transpiled/react/Table/Virtualized/Dnd/DnDConfigWrapper.d.ts +2 -0
  65. package/transpiled/react/Table/Virtualized/Dnd/DnDConfigWrapper.js +55 -0
  66. package/transpiled/react/Table/Virtualized/Dnd/TableRow.d.ts +8 -0
  67. package/transpiled/react/Table/Virtualized/Dnd/TableRow.js +130 -0
  68. package/transpiled/react/Table/Virtualized/FixedHeaderContent.d.ts +10 -0
  69. package/transpiled/react/Table/Virtualized/FixedHeaderContent.js +54 -0
  70. package/transpiled/react/Table/Virtualized/HeadCell.d.ts +8 -0
  71. package/transpiled/react/Table/Virtualized/HeadCell.js +44 -0
  72. package/transpiled/react/Table/Virtualized/RowContent.d.ts +10 -0
  73. package/transpiled/react/Table/Virtualized/RowContent.js +34 -0
  74. package/transpiled/react/Table/Virtualized/helpers.d.ts +2 -0
  75. package/transpiled/react/Table/Virtualized/helpers.js +64 -0
  76. package/transpiled/react/Table/Virtualized/helpers.spec.d.ts +1 -0
  77. package/transpiled/react/Table/Virtualized/index.d.ts +2 -0
  78. package/transpiled/react/Table/Virtualized/index.js +115 -0
  79. package/transpiled/react/Table/Virtualized/virtuosoComponents.d.ts +10 -0
  80. package/transpiled/react/Table/Virtualized/virtuosoComponents.js +100 -0
  81. package/transpiled/react/TableRow/index.d.ts +2 -1
  82. package/transpiled/react/TableRow/index.js +20 -1
  83. package/transpiled/react/stylesheet.css +1 -1
@@ -0,0 +1,80 @@
1
+ ### React-Virtuoso
2
+
3
+ ```jsx
4
+ import VirtualizedTable from 'cozy-ui/transpiled/react/Table/Virtualized'
5
+
6
+ const createData = (id, name, calories, fat, carbs, protein) => {
7
+ return { id, name, calories, fat, carbs, protein }
8
+ }
9
+
10
+ const rows = [
11
+ createData(0, 'Cupcake', 305, 3.7, 67, 4.3),
12
+ createData(1, 'Donut', 452, 25.0, 51, 4.9),
13
+ createData(2, 'Eclair', 262, 16.0, 24, 6.0),
14
+ createData(3, 'Frozen yoghurt', 159, 6.0, 24, 4.0),
15
+ createData(4, 'Gingerbread', 356, 16.0, 49, 3.9),
16
+ createData(5, 'Honeycomb', 408, 3.2, 87, 6.5),
17
+ createData(6, 'Ice cream sandwich', 237, 9.0, 37, 4.3),
18
+ createData(7, 'Jelly Bean', 375, 0.0, 94, 0.0),
19
+ createData(8, 'KitKat', 518, 26.0, 65, 7.0),
20
+ createData(9, 'Lollipop', 392, 0.2, 98, 0.0),
21
+ createData(10, 'Marshmallow', 318, 0, 81, 2.0),
22
+ createData(11, 'Nougat', 360, 19.0, 9, 37.0),
23
+ createData(12, 'Oreo', 437, 18.0, 63, 4.0),
24
+ createData(
25
+ 6,
26
+ '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',
27
+ 237,
28
+ 9.0,
29
+ 37,
30
+ 4.3
31
+ )
32
+ ]
33
+
34
+ const columns = [
35
+ {
36
+ id: 'name',
37
+ disablePadding: true,
38
+ label: 'Dessert'
39
+ },
40
+ {
41
+ id: 'calories',
42
+ disablePadding: false,
43
+ width: 80,
44
+ label: 'Calories',
45
+ textAlign: 'left',
46
+ sortable: false
47
+ },
48
+ {
49
+ id: 'fat',
50
+ disablePadding: false,
51
+ width: 85,
52
+ label: 'Fat (g)',
53
+ textAlign: 'right'
54
+ },
55
+ {
56
+ id: 'carbs',
57
+ disablePadding: false,
58
+ width: 115,
59
+ label: 'Carbs (g)',
60
+ textAlign: 'right'
61
+ },
62
+ {
63
+ id: 'protein',
64
+ disablePadding: false,
65
+ width: 115,
66
+ label: 'Protein (g)',
67
+ textAlign: 'right'
68
+ }
69
+ ]
70
+
71
+ ;
72
+
73
+ <div style={{ border: "1px solid var(--borderMainColor)", height: 400, width: "100%" }}>
74
+ <VirtualizedTable
75
+ rows={rows}
76
+ columns={columns}
77
+ defaultOrder={columns[0].id}
78
+ />
79
+ </div>
80
+ ```
@@ -0,0 +1,41 @@
1
+ import cx from 'classnames'
2
+ import React from 'react'
3
+
4
+ import TableCell from '../../TableCell'
5
+ import { makeStyles } from '../../styles'
6
+
7
+ const useStyles = makeStyles({
8
+ root: {
9
+ width: ({ column }) => column.width,
10
+ maxWidth: ({ column }) => column.maxWidth
11
+ }
12
+ })
13
+
14
+ const Cell = ({ row, columns, column, children }) => {
15
+ const classes = useStyles({ column })
16
+
17
+ return (
18
+ <TableCell
19
+ key={column.id}
20
+ classes={classes}
21
+ className={cx({ sortable: column.sortable !== false })}
22
+ align={column.textAlign ?? 'left'}
23
+ padding={column.disablePadding ? 'none' : 'normal'}
24
+ >
25
+ {children
26
+ ? React.Children.map(children, child =>
27
+ React.isValidElement(child)
28
+ ? React.cloneElement(child, {
29
+ row,
30
+ columns,
31
+ column,
32
+ cell: row[column.id]
33
+ })
34
+ : null
35
+ )
36
+ : row[column.id]}
37
+ </TableCell>
38
+ )
39
+ }
40
+
41
+ export default React.memo(Cell)
@@ -0,0 +1,45 @@
1
+ import React from 'react'
2
+ import { useDragLayer } from 'react-dnd'
3
+
4
+ import DragPreviewWrapper from './DragPreviewWrapper'
5
+
6
+ const layerStyles = {
7
+ position: 'fixed',
8
+ pointerEvents: 'none',
9
+ zIndex: 100,
10
+ left: 0,
11
+ top: 0,
12
+ width: '100%',
13
+ height: '100%'
14
+ }
15
+
16
+ // Example find in the official documentation
17
+ // https://react-dnd.github.io/react-dnd/examples/drag-around/custom-drag-layer
18
+ export const CustomDragLayer = ({ dragId }) => {
19
+ const { itemType, isDragging, item, initialOffset, currentOffset } =
20
+ useDragLayer(monitor => ({
21
+ item: monitor.getItem(),
22
+ itemType: monitor.getItemType(),
23
+ initialOffset: monitor.getInitialSourceClientOffset(),
24
+ currentOffset: monitor.getSourceClientOffset(),
25
+ isDragging: monitor.isDragging()
26
+ }))
27
+
28
+ if (!isDragging) {
29
+ return null
30
+ }
31
+
32
+ return (
33
+ <div style={layerStyles}>
34
+ <DragPreviewWrapper
35
+ item={item}
36
+ itemType={itemType}
37
+ dragId={dragId}
38
+ initialOffset={initialOffset}
39
+ currentOffset={currentOffset}
40
+ />
41
+ </div>
42
+ )
43
+ }
44
+
45
+ export default CustomDragLayer
@@ -0,0 +1,43 @@
1
+ import React from 'react'
2
+
3
+ import Badge from '../../../../Badge'
4
+ import Paper from '../../../../Paper'
5
+ import Typography from '../../../../Typography'
6
+ import { makeStyles } from '../../../../styles'
7
+
8
+ const useStyles = makeStyles({
9
+ root: {
10
+ width: 'fit-content'
11
+ }
12
+ })
13
+
14
+ const DragPreview = ({ fileName, selectedCount }) => {
15
+ const classes = useStyles()
16
+
17
+ return (
18
+ <>
19
+ {selectedCount > 1 ? (
20
+ <Badge
21
+ badgeContent={selectedCount}
22
+ size="large"
23
+ color="primary"
24
+ anchorOrigin={{
25
+ vertical: 'top',
26
+ horizontal: 'right'
27
+ }}
28
+ overlap="rectangular"
29
+ >
30
+ <Paper classes={classes} className="u-p-half u-maw-5">
31
+ <Typography>{fileName}</Typography>
32
+ </Paper>
33
+ </Badge>
34
+ ) : (
35
+ <Paper classes={classes} className="u-p-half u-maw-5">
36
+ <Typography>{fileName}</Typography>
37
+ </Paper>
38
+ )}
39
+ </>
40
+ )
41
+ }
42
+
43
+ export default React.memo(DragPreview)
@@ -0,0 +1,52 @@
1
+ import React, { useEffect, useState } from 'react'
2
+
3
+ import DragPreview from './DragPreview'
4
+
5
+ const makeStyles = ({ x, y }) => {
6
+ if (!x || !y) {
7
+ return { display: 'none' }
8
+ }
9
+
10
+ const transform = `translate(${x}px, ${y}px)`
11
+
12
+ return {
13
+ transform,
14
+ WebkitTransform: transform
15
+ }
16
+ }
17
+
18
+ const DragPreviewWrapper = ({
19
+ item,
20
+ itemType,
21
+ dragId,
22
+ initialOffset,
23
+ currentOffset
24
+ }) => {
25
+ const [mousePosition, setMousePosition] = useState({ x: null, y: null })
26
+
27
+ useEffect(() => {
28
+ const handleMouseMove = e => {
29
+ setMousePosition({ x: e.clientX, y: e.clientY })
30
+ }
31
+
32
+ window.addEventListener('dragover', handleMouseMove)
33
+ return () => {
34
+ window.removeEventListener('dragover', handleMouseMove)
35
+ }
36
+ }, [])
37
+
38
+ if (!initialOffset || !currentOffset || itemType !== dragId) {
39
+ return null
40
+ }
41
+
42
+ return (
43
+ <div style={makeStyles(mousePosition)}>
44
+ <DragPreview
45
+ fileName={item.draggedItems[0].name}
46
+ selectedCount={item.draggedItems.length}
47
+ />
48
+ </div>
49
+ )
50
+ }
51
+
52
+ export default DragPreviewWrapper
@@ -0,0 +1,48 @@
1
+ import { forwardRef, useEffect, useState } from 'react'
2
+ import { useDragDropManager } from 'react-dnd'
3
+
4
+ const DnDConfigWrapper = forwardRef(({ children }, ref) => {
5
+ const dragDropManager = useDragDropManager()
6
+ const monitor = dragDropManager.getMonitor()
7
+ const [isDragging, setIsDragging] = useState(false)
8
+
9
+ useEffect(() => {
10
+ const unsubscribe = monitor.subscribeToStateChange(() => {
11
+ setIsDragging(monitor.isDragging())
12
+ })
13
+ return () => unsubscribe()
14
+ }, [monitor])
15
+
16
+ useEffect(() => {
17
+ if (!isDragging) return
18
+
19
+ const scrollThreshold = 100
20
+ const scrollMaxSpeed = 75
21
+
22
+ const intervalId = setInterval(() => {
23
+ const offset = monitor.getClientOffset()
24
+ const container = ref.current
25
+ if (!offset || !container) return
26
+
27
+ const { top, bottom } = container.getBoundingClientRect()
28
+ const distanceToTop = offset.y - top
29
+ const distanceToBottom = bottom - offset.y
30
+
31
+ if (distanceToTop < scrollThreshold) {
32
+ const speed = scrollMaxSpeed * (1 - distanceToTop / scrollThreshold)
33
+ container.scrollBy(0, -speed)
34
+ } else if (distanceToBottom < scrollThreshold) {
35
+ const speed = scrollMaxSpeed * (1 - distanceToBottom / scrollThreshold)
36
+ container.scrollBy(0, speed)
37
+ }
38
+ }, 16) // ~60fps
39
+
40
+ return () => clearInterval(intervalId)
41
+ }, [isDragging, monitor, ref])
42
+
43
+ return children
44
+ })
45
+
46
+ DnDConfigWrapper.displayName = 'DnDConfigWrapper'
47
+
48
+ export default DnDConfigWrapper
@@ -0,0 +1,86 @@
1
+ import React, { useEffect } from 'react'
2
+ import { useDrag, useDrop } from 'react-dnd'
3
+ import { getEmptyImage } from 'react-dnd-html5-backend'
4
+
5
+ import TableRowClassic from '../../../TableRow'
6
+
7
+ const TableRow = ({ item, context, selected, disabled, ...props }) => {
8
+ const { selectedItems, setItemsInDropProcess, dragProps } = context
9
+ const {
10
+ onDrop,
11
+ canDrop: canDropProps,
12
+ canDrag: canDragProps,
13
+ dragId
14
+ } = dragProps
15
+
16
+ const [dragCollect, dragRef, dragRefPreview] = useDrag(
17
+ () => ({
18
+ type: dragId,
19
+ isDragging: monitor => {
20
+ // put all selected items in isDragging state
21
+ if (selectedItems.length > 0) {
22
+ return selectedItems.some(
23
+ selectedItem => selectedItem._id === item._id
24
+ )
25
+ }
26
+
27
+ return item._id === monitor.getItem().draggedItems[0]._id
28
+ },
29
+ item: {
30
+ draggedItems: selectedItems.length > 0 ? selectedItems : [item]
31
+ },
32
+ canDrag: () => {
33
+ const defaultCanDrag = canDragProps?.(item) || true
34
+ // if selectedItems is not empty, only the selected items can be dragged
35
+ if (selectedItems.length > 0) {
36
+ return defaultCanDrag && selected
37
+ }
38
+ return defaultCanDrag
39
+ },
40
+ collect: monitor => {
41
+ return {
42
+ isDragging: monitor.isDragging()
43
+ }
44
+ }
45
+ }),
46
+ [item, selectedItems] // used to pass args inside returned object attributes
47
+ )
48
+
49
+ const [dropCollect, dropRef] = useDrop(
50
+ () => ({
51
+ accept: dragId,
52
+ canDrop: () => (canDropProps ? canDropProps(item) : true),
53
+ drop: async draggedItem => {
54
+ setItemsInDropProcess(
55
+ draggedItem.draggedItems.map(draggedItem => draggedItem._id)
56
+ )
57
+ await onDrop(draggedItem.draggedItems, item, selectedItems)
58
+ setItemsInDropProcess([])
59
+ },
60
+ collect: monitor => {
61
+ return {
62
+ isOver: monitor.isOver()
63
+ }
64
+ }
65
+ }),
66
+ [item._id, selectedItems] // used to pass args inside returned object attributes
67
+ )
68
+
69
+ // Tricks for the preview image to be empty
70
+ // https://react-dnd.github.io/react-dnd/examples/drag-around/custom-drag-layer
71
+ useEffect(() => {
72
+ dragRefPreview(getEmptyImage(), { captureDraggingState: true })
73
+ }, [dragRefPreview])
74
+
75
+ return (
76
+ <TableRowClassic
77
+ {...props}
78
+ ref={node => dragRef(dropRef(node))}
79
+ selected={selected || dropCollect.isOver}
80
+ className={dragCollect.isDragging ? 'u-o-50' : ''}
81
+ disabled={disabled}
82
+ />
83
+ )
84
+ }
85
+
86
+ export default TableRow
@@ -0,0 +1,58 @@
1
+ import React from 'react'
2
+
3
+ import TableHeadCell from './HeadCell'
4
+ import Checkbox from '../../Checkbox'
5
+ import TableCell from '../../TableCell'
6
+ import TableRow from '../../TableRow'
7
+ import { makeStyles } from '../../styles'
8
+
9
+ const useStyles = makeStyles({
10
+ visuallyHidden: {
11
+ border: 0,
12
+ clip: 'rect(0 0 0 0)',
13
+ height: 1,
14
+ margin: -1,
15
+ overflow: 'hidden',
16
+ padding: 0,
17
+ position: 'absolute',
18
+ top: 20,
19
+ width: 1
20
+ }
21
+ })
22
+
23
+ const FixedHeaderContent = ({
24
+ columns,
25
+ order,
26
+ orderBy,
27
+ rowCount,
28
+ selectedCount,
29
+ onClick,
30
+ onSelectAllClick
31
+ }) => {
32
+ const classes = useStyles()
33
+
34
+ return (
35
+ <TableRow>
36
+ <TableCell align="center" padding="checkbox">
37
+ <Checkbox
38
+ indeterminate={selectedCount > 0 && selectedCount < rowCount}
39
+ checked={rowCount > 0 && selectedCount === rowCount}
40
+ inputProps={{ 'aria-label': 'select all' }}
41
+ onChange={onSelectAllClick}
42
+ />
43
+ </TableCell>
44
+ {columns.map(column => (
45
+ <TableHeadCell
46
+ key={column.id}
47
+ className={classes.visuallyHidden}
48
+ column={column}
49
+ order={order}
50
+ orderBy={orderBy}
51
+ onClick={() => onClick(column.id)}
52
+ />
53
+ ))}
54
+ </TableRow>
55
+ )
56
+ }
57
+
58
+ export default FixedHeaderContent
@@ -0,0 +1,45 @@
1
+ import React from 'react'
2
+
3
+ import TableCell from '../../TableCell'
4
+ import TableSortLabel from '../../TableSortLabel'
5
+ import { makeStyles } from '../../styles'
6
+
7
+ const useStyles = makeStyles({
8
+ root: {
9
+ width: ({ column }) => column.width,
10
+ maxWidth: ({ column }) => column.maxWidth
11
+ }
12
+ })
13
+
14
+ const TableHeadCell = ({ className, column, orderBy, order, onClick }) => {
15
+ const classes = useStyles({ column })
16
+
17
+ return (
18
+ <TableCell
19
+ key={column.id}
20
+ classes={classes}
21
+ align={column.textAlign ?? 'left'}
22
+ padding={column.disablePadding ? 'none' : 'normal'}
23
+ sortDirection={orderBy === column.id ? order : false}
24
+ >
25
+ {column.sortable !== false ? (
26
+ <TableSortLabel
27
+ active={orderBy === column.id}
28
+ direction={orderBy === column.id ? order : 'asc'}
29
+ onClick={onClick}
30
+ >
31
+ {column.label}
32
+ {orderBy === column.id && (
33
+ <span className={className}>
34
+ {order === 'desc' ? 'sorted descending' : 'sorted ascending'}
35
+ </span>
36
+ )}
37
+ </TableSortLabel>
38
+ ) : (
39
+ column.label
40
+ )}
41
+ </TableCell>
42
+ )
43
+ }
44
+
45
+ export default TableHeadCell
@@ -0,0 +1,35 @@
1
+ import React from 'react'
2
+
3
+ import Cell from './Cell'
4
+ import Checkbox from '../../Checkbox'
5
+ import TableCell from '../../TableCell'
6
+
7
+ const RowContent = ({
8
+ index,
9
+ row,
10
+ columns,
11
+ isSelectedItem,
12
+ children,
13
+ onSelectClick
14
+ }) => {
15
+ return (
16
+ <>
17
+ <TableCell align="center" padding="checkbox">
18
+ <Checkbox
19
+ checked={isSelectedItem(row)}
20
+ inputProps={{
21
+ 'aria-labelledby': `enhanced-table-checkbox-${index}`
22
+ }}
23
+ onChange={() => onSelectClick(row)}
24
+ />
25
+ </TableCell>
26
+ {columns.map(column => (
27
+ <Cell key={column.id} row={row} columns={columns} column={column}>
28
+ {children}
29
+ </Cell>
30
+ ))}
31
+ </>
32
+ )
33
+ }
34
+
35
+ export default React.memo(RowContent)
@@ -0,0 +1,41 @@
1
+ const descendingComparator = ({ a, b, order, orderBy, lang }) => {
2
+ const aValue = a[orderBy]
3
+ const bValue = b[orderBy]
4
+
5
+ if (typeof aValue === 'string') {
6
+ const isDate = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}/.test(aValue)
7
+ const isNumber = !isNaN(parseInt(aValue))
8
+
9
+ if (isDate) {
10
+ return new Date(bValue) - new Date(aValue)
11
+ }
12
+
13
+ if (isNumber) {
14
+ return parseInt(bValue) - parseInt(aValue)
15
+ }
16
+
17
+ const { compare } = Intl.Collator(lang || 'en', {
18
+ caseFirst: order === 'asc' ? 'upper' : 'lower'
19
+ })
20
+
21
+ return compare(bValue, aValue)
22
+ }
23
+
24
+ return bValue - aValue
25
+ }
26
+
27
+ export const getComparator = (order, orderBy, lang) => {
28
+ return order === 'desc'
29
+ ? (a, b) => descendingComparator({ a, b, order, orderBy, lang })
30
+ : (a, b) => -descendingComparator({ a, b, order, orderBy, lang })
31
+ }
32
+
33
+ export const stableSort = (array, comparator) => {
34
+ const stabilizedThis = array.map((el, index) => [el, index])
35
+ stabilizedThis.sort((a, b) => {
36
+ const order = comparator(a[0], b[0])
37
+ if (order !== 0) return order
38
+ return a[1] - b[1]
39
+ })
40
+ return stabilizedThis.map(el => el[0])
41
+ }
@@ -0,0 +1,108 @@
1
+ import { stableSort, getComparator } from './helpers'
2
+
3
+ describe('stableSort', () => {
4
+ describe('it should sort string correctly even with number', () => {
5
+ const rows = [
6
+ { name: 'b' },
7
+ { name: 'A' },
8
+ { name: 'B' },
9
+ { name: 'a' },
10
+ { name: '10' }
11
+ ]
12
+
13
+ it('for asc', () => {
14
+ const sortedData = stableSort(rows, getComparator('asc', 'name', 'fr'))
15
+
16
+ const onlyValues = sortedData.map(el => el.name)
17
+ expect(onlyValues).toStrictEqual(['A', 'a', 'B', 'b', '10'])
18
+ })
19
+
20
+ it('for desc', () => {
21
+ const sortedData = stableSort(rows, getComparator('desc', 'name', 'fr'))
22
+
23
+ const onlyValues = sortedData.map(el => el.name)
24
+ expect(onlyValues).toStrictEqual(['B', 'b', 'A', 'a', '10'])
25
+ })
26
+ })
27
+
28
+ describe('it should sort number correctly', () => {
29
+ const rows = [
30
+ { number: 40 },
31
+ { number: 1 },
32
+ { number: 8 },
33
+ { number: 30 },
34
+ { number: 40 }
35
+ ]
36
+
37
+ it('for asc', () => {
38
+ const sortedData = stableSort(rows, getComparator('asc', 'number', 'fr'))
39
+
40
+ const onlyValues = sortedData.map(el => el.number)
41
+ expect(onlyValues).toStrictEqual([1, 8, 30, 40, 40])
42
+ })
43
+
44
+ it('for desc', () => {
45
+ const sortedData = stableSort(rows, getComparator('desc', 'number', 'fr'))
46
+
47
+ const onlyValues = sortedData.map(el => el.number)
48
+ expect(onlyValues).toStrictEqual([40, 40, 30, 8, 1])
49
+ })
50
+ })
51
+
52
+ describe('it should sort string-number correctly', () => {
53
+ const rows = [
54
+ { number: '40' },
55
+ { number: '1' },
56
+ { number: '8' },
57
+ { number: '30' },
58
+ { number: '40' }
59
+ ]
60
+
61
+ it('for asc', () => {
62
+ const sortedData = stableSort(rows, getComparator('asc', 'number', 'fr'))
63
+
64
+ const onlyValues = sortedData.map(el => el.number)
65
+ expect(onlyValues).toStrictEqual(['1', '8', '30', '40', '40'])
66
+ })
67
+
68
+ it('for desc', () => {
69
+ const sortedData = stableSort(rows, getComparator('desc', 'number', 'fr'))
70
+
71
+ const onlyValues = sortedData.map(el => el.number)
72
+ expect(onlyValues).toStrictEqual(['40', '40', '30', '8', '1'])
73
+ })
74
+ })
75
+
76
+ describe('it should sort date correctly', () => {
77
+ const rows = [
78
+ { date: '2025-05-01T12:00:00.0000+01:00' },
79
+ { date: '2025-08-01T12:00:00.0000+01:00' },
80
+ { date: '2025-01-01T12:00:00.0000+01:00' },
81
+ { date: '2025-04-01T12:00:00.0000+01:00' }
82
+ ]
83
+
84
+ it('for asc', () => {
85
+ const sortedData = stableSort(rows, getComparator('asc', 'date', 'fr'))
86
+
87
+ const onlyValues = sortedData.map(el => el.date)
88
+ expect(onlyValues).toStrictEqual([
89
+ '2025-01-01T12:00:00.0000+01:00',
90
+ '2025-04-01T12:00:00.0000+01:00',
91
+ '2025-05-01T12:00:00.0000+01:00',
92
+ '2025-08-01T12:00:00.0000+01:00'
93
+ ])
94
+ })
95
+
96
+ it('for desc', () => {
97
+ const sortedData = stableSort(rows, getComparator('desc', 'date', 'fr'))
98
+
99
+ const onlyValues = sortedData.map(el => el.date)
100
+ expect(onlyValues).toStrictEqual([
101
+ '2025-08-01T12:00:00.0000+01:00',
102
+ '2025-05-01T12:00:00.0000+01:00',
103
+ '2025-04-01T12:00:00.0000+01:00',
104
+ '2025-01-01T12:00:00.0000+01:00'
105
+ ])
106
+ })
107
+ })
108
+ })