From 4332541576950d2578630008fb2c9a939e25bf09 Mon Sep 17 00:00:00 2001 From: Alexandre Monjol Date: Tue, 3 Dec 2024 16:03:39 +0100 Subject: [PATCH] misc(Table): migrate to Tailwind --- src/components/designSystem/Table/Table.tsx | 538 +++++++++--------- .../utils/__tests__/responsiveProps.test.ts | 41 ++ src/core/utils/responsiveProps.ts | 16 +- tailwind.config.ts | 19 + 4 files changed, 333 insertions(+), 281 deletions(-) create mode 100644 src/core/utils/__tests__/responsiveProps.test.ts diff --git a/src/components/designSystem/Table/Table.tsx b/src/components/designSystem/Table/Table.tsx index 752bd3d82..9193c3a41 100644 --- a/src/components/designSystem/Table/Table.tsx +++ b/src/components/designSystem/Table/Table.tsx @@ -1,13 +1,13 @@ import { - Box, Table as MUITable, TableBody as MUITableBody, TableCell as MUITableCell, TableHead as MUITableHead, TableRow as MUITableRow, + TableCellProps, + TableRowProps, } from '@mui/material' -import { MouseEvent, ReactNode, useRef } from 'react' -import styled from 'styled-components' +import { MouseEvent, PropsWithChildren, ReactNode, useRef } from 'react' import { Button, IconName, Popper, Skeleton, Tooltip, Typography } from '~/components/designSystem' import { GenericPlaceholder, GenericPlaceholderProps } from '~/components/GenericPlaceholder' @@ -17,6 +17,7 @@ import { useListKeysNavigation } from '~/hooks/ui/useListKeyNavigation' import EmptyImage from '~/public/images/maneki/empty.svg' import ErrorImage from '~/public/images/maneki/error.svg' import { MenuPopper, PopperOpener, theme } from '~/styles' +import { tw } from '~/styles/utils' const PADDING_SPACING_RIGHT_PX = 32 @@ -92,6 +93,227 @@ const countMaxSpaceColumns = (columns: TableColumn[]) => return acc }, 0) +const TableRow = ({ + children, + className, + isClickable, + ...props +}: TableRowProps & { + isClickable?: boolean +}) => { + return ( + + {children} + + ) +} + +const TableCell = ({ + children, + className, + hasPlaceholderDisplayed, + isBlurred, + maxSpace, + ...props +}: PropsWithChildren & + TableCellProps & { + className?: string + isBlurred?: boolean + hasPlaceholderDisplayed?: boolean + maxSpace?: number + }) => { + return ( + div': { + paddingRight: `${PADDING_SPACING_RIGHT_PX}px`, + }, + '&:first-of-type > div': { + paddingLeft: 0, + }, + '&:last-of-type > div': { + paddingRight: 0, + }, + }} + {...props} + > + {children} + + ) +} + +const TableActionCell = ({ + children, + className, + ...props +}: PropsWithChildren & TableCellProps & { className?: string }) => { + return ( + div': { + justifyContent: 'center', + paddingLeft: theme.spacing(3), + }, + }} + {...props} + > + {children} + + ) +} + +const TableInnerCell = ({ + align, + children, + className, + minWidth, + style, + truncateOverflow, +}: PropsWithChildren & { + align?: Align + className?: string + minWidth?: number + style?: React.CSSProperties + truncateOverflow?: boolean +}) => { + return ( +
+ {children} +
+ ) +} + +const LoadingRows = ({ + columns, + id, + shouldDisplayActionColumn, + actionColumn, +}: Pick, 'actionColumn'> & { + columns: Array> + id: string + shouldDisplayActionColumn: boolean +}) => { + return Array.from({ length: LOADING_ROW_COUNT }).map((_, i) => ( + + {columns.map((col, j) => ( + + +
+ +
+
+
+ ))} + {shouldDisplayActionColumn && ( + + + {Array.isArray(actionColumn?.({} as T)) ? ( + + ) + + const withTooltip = ( + + {button} + + ) + + if (action.tooltip) { + return withTooltip + } + + return button +} + export const Table = ({ name, data, @@ -173,7 +395,7 @@ export const Table = ({ if (hasError) { return ( - + ({ if (!isLoading && data.length === 0) { return ( - + ({ return ( // Width is set to 0 and minWidth to 100% to prevent table from overflowing its container // cf. https://stackoverflow.com/a/73091777 - - + - + <> {filteredColumns.map((column, i) => ( - {column.title} + + {column.title} + ))} {shouldDisplayActionColumn && } - + {renderPlaceholder() ?? @@ -257,7 +492,7 @@ export const Table = ({ key={`${TABLE_ID}-row-${i}`} id={`${TABLE_ID}-row-${i}`} data-id={item.id} - $isClickable={isClickable} + isClickable={isClickable} tabIndex={isClickable ? 0 : undefined} onKeyDown={isClickable ? onKeyDown : undefined} onClick={isClickable ? (e) => handleRowClick(e, item) : undefined} @@ -266,12 +501,12 @@ export const Table = ({ {column.content(item)} @@ -285,15 +520,16 @@ export const Table = ({ popperGroupName={`${TABLE_ID}-action-cell`} PopperProps={{ placement: 'bottom-end' }} opener={({ isOpen }) => ( - + - ) - - const withTooltip = ( - - {button} - - ) - - if (action.tooltip) { - return withTooltip - } - - return button -} - -const TableInnerCell = styled.div<{ - $minWidth?: number - $align?: Align - $truncateOverflow?: boolean -}>` - min-width: ${({ $minWidth }) => ($minWidth ? `${$minWidth}px` : 'auto')}; - display: ${({ $truncateOverflow }) => ($truncateOverflow ? 'grid' : 'flex')}; - align-items: center; - justify-content: ${({ $align }) => { - if ($align === 'left') return 'flex-start' - if ($align === 'center') return 'center' - if ($align === 'right') return 'flex-end' - }}; -` - -const TableCell = styled(MUITableCell)<{ - $isBlurred?: boolean - $hasPlaceholderDisplayed?: boolean - $maxSpace?: number -}>` - width: ${({ $maxSpace }) => ($maxSpace ? `${$maxSpace}%` : 'auto')}; - padding: 0; - box-sizing: border-box; - white-space: nowrap; - border-bottom: ${({ $hasPlaceholderDisplayed }) => - $hasPlaceholderDisplayed ? 'none' : `1px solid ${theme.palette.grey[300]}`}; - - ${TableInnerCell} { - padding-right: ${PADDING_SPACING_RIGHT_PX}px; - } - - &:first-of-type ${TableInnerCell} { - padding-left: 0; - } - - &:last-of-type ${TableInnerCell} { - padding-right: 0; - } -` - -const StyledTable = styled(MUITable)<{ - $containerSize: ResponsiveStyleValue - $rowSize: RowSize -}>` - border-collapse: collapse; - - ${TableCell}:first-of-type ${TableInnerCell} { - ${({ $containerSize }) => setResponsiveProperty('paddingLeft', $containerSize)} - } - - ${TableCell}:last-of-type ${TableInnerCell} { - ${({ $containerSize }) => setResponsiveProperty('paddingRight', $containerSize)} - } - - ${TableInnerCell} { - min-height: ${({ $rowSize }) => $rowSize}px; - } -` - -const TableActionCell = styled(TableCell)` - width: 40px; - position: sticky; - right: 0; - z-index: 1; - box-shadow: none; - background-color: ${theme.palette.background.paper}; - - ${TableInnerCell} { - justify-content: center; - padding-left: ${theme.spacing(3)}; - } - - @supports (animation-timeline: scroll(inline)) { - animation-name: shadow; - animation-duration: 1s; - animation-timing-function: ease-in-out; - animation-timeline: scroll(inline); - } - - @keyframes shadow { - 0% { - box-shadow: ${theme.shadows[8]}; - } - - 90% { - box-shadow: ${theme.shadows[8]}; - } - - 99% { - box-shadow: none; - } - } -` - -const TableHead = styled(MUITableHead)` - ${TableCell} { - background-color: ${theme.palette.background.paper}; - position: sticky; - top: 0; - z-index: 1; - border-bottom: none; - box-shadow: ${theme.shadows[7]}; - } - - ${TableInnerCell} { - font-size: ${theme.typography.captionHl.fontSize}; - font-weight: ${theme.typography.captionHl.fontWeight}; - line-height: ${theme.typography.captionHl.lineHeight}; - color: ${theme.palette.grey[600]}; - min-height: 40px; - } - - ${TableActionCell} { - z-index: 10; - - &::after { - content: ''; - display: block; - position: absolute; - bottom: 0; - width: 100%; - height: 1px; - box-shadow: ${theme.shadows[7]}; - } - } -` - -const TableRow = styled(MUITableRow)<{ $isClickable?: boolean }>` - cursor: ${({ $isClickable }) => ($isClickable ? 'pointer' : 'initial')}; - - &:hover:not(:active), - &:focus:not(:active), - &:hover:not(:active) ${TableActionCell}, &:focus:not(:active) ${TableActionCell} { - background-color: ${({ $isClickable }) => - $isClickable ? `${theme.palette.grey[100]}` : undefined}; - } - - &:active, - &:active ${TableActionCell} { - background-color: ${({ $isClickable }) => - $isClickable ? `${theme.palette.grey[200]}` : undefined}; - } - - // Remove hover effect when action column is hovered - &:has([data-id='${ACTION_COLUMN_ID}'] button:hover) { - background-color: unset !important; - - ${TableActionCell} { - background-color: ${theme.palette.background.paper}; - } - } -` - -const LocalPopperOpener = styled(PopperOpener)` - position: relative; - top: 0; - right: 0; - height: 100%; - > *:first-child { - right: 0; - } -` diff --git a/src/core/utils/__tests__/responsiveProps.test.ts b/src/core/utils/__tests__/responsiveProps.test.ts new file mode 100644 index 000000000..46c9aa14b --- /dev/null +++ b/src/core/utils/__tests__/responsiveProps.test.ts @@ -0,0 +1,41 @@ +import { setResponsiveProperty } from '../responsiveProps' + +describe('setResponsiveProperty', () => { + it('should return a responsive style object', () => { + expect(setResponsiveProperty('margin', 0)).toEqual({ + margin: '0px', + }) + + expect(setResponsiveProperty('margin', { default: 4 })).toEqual({ + margin: '4px', + }) + + expect( + setResponsiveProperty('margin', { + default: 4, + md: 48, + }), + ).toEqual({ + margin: '4px', + '@media (min-width:776px)': { + margin: '48px', + }, + }) + + expect( + setResponsiveProperty('margin', { + default: 20, + md: 30, + lg: 10, + }), + ).toEqual({ + margin: '20px', + '@media (min-width:776px)': { + margin: '30px', + }, + '@media (min-width:1024px)': { + margin: '10px', + }, + }) + }) +}) diff --git a/src/core/utils/responsiveProps.ts b/src/core/utils/responsiveProps.ts index 70ae85227..c93c965c7 100644 --- a/src/core/utils/responsiveProps.ts +++ b/src/core/utils/responsiveProps.ts @@ -1,5 +1,3 @@ -import { css } from 'styled-components' - import { theme } from '~/styles' type Breakpoint = keyof typeof theme.breakpoints.values @@ -24,18 +22,18 @@ export const setResponsiveProperty = ( ...curr, [theme.breakpoints.up(breakpoint)]: { - [cssProperty]: value[breakpoint] ?? defaultValue, + [cssProperty]: !!value[breakpoint] ? `${value[breakpoint]}px` : `${defaultValue}px`, }, } }, {}) - return css({ - [cssProperty]: defaultValue, + return { ...responsiveCssProperties, - }) + [cssProperty]: `${defaultValue}px`, + } } - return css({ - [cssProperty]: value, - }) + return { + [cssProperty]: `${value}px`, + } } diff --git a/tailwind.config.ts b/tailwind.config.ts index 2e2d4298d..c01344801 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -216,6 +216,25 @@ const config = { boxShadow: `1px 0px 0px 0px ${theme('colors.grey.300')} inset`, }, }) + + // Animation + addUtilities({ + '.animate-shadow-left': { + '@supports (animation-timeline: scroll(inline))': { + animationName: 'shadowLeft', + animationDuration: '1s', + animationTimingFunction: 'ease-in-out', + animationTimeline: 'scroll(inline)', + }, + + '@keyframes shadowLeft': { + '0%': { boxShadow: `1px 0px 0px 0px ${theme('colors.grey.300')} inset` }, + '90%': { boxShadow: `1px 0px 0px 0px ${theme('colors.grey.300')} inset` }, + '99%': { boxShadow: 'none' }, + }, + }, + }) + // Outline ring addUtilities({ '.ring': {