From 6cc137e187bd0314756bfc973f65bcde19f063ea Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 23 May 2024 16:23:22 +0200 Subject: [PATCH] feat(tables): add $isLoading prop to SimpleTable.Td - add `$isLoading` prop to `SimpleTable.Td (& `TableWithSelectableRows.Td`) - add `$width` prop to `SimpleTable.Th` (& `TableWithSelectableRows.Th`) - add & expose `SimpleTable.CellLoader` (& `TableWithSelectableRows.CellLoader`) - fix ellipsis when `SimpleTable.Th` width is set while not set in `SimpleTable.Td` (// `TableWithSelectableRows`) - deprecate `$width` prop from `TableWithSelectableRows.Td` --- config/jest.config.js | 1 + src/tables/SimpleTable/CellLoader.tsx | 29 ++++++++ src/tables/SimpleTable/Td.tsx | 25 +++++++ .../index.tsx} | 55 +++++++-------- src/tables/TableWithSelectableRows/index.tsx | 36 ++++++---- .../CheckPicker/WithCustomSearch.stories.tsx | 2 +- .../WithThreeColumns.stories.tsx | 4 +- .../MultiSelect/WithCustomSearch.stories.tsx | 2 +- .../Select/WithCustomSearch.stories.tsx | 2 +- .../tables/SimpleTable/WithLoader.stories.tsx | 70 +++++++++++++++++++ tsconfig.json | 1 + 11 files changed, 177 insertions(+), 50 deletions(-) create mode 100644 src/tables/SimpleTable/CellLoader.tsx create mode 100644 src/tables/SimpleTable/Td.tsx rename src/tables/{SimpleTable.tsx => SimpleTable/index.tsx} (73%) create mode 100644 stories/tables/SimpleTable/WithLoader.stories.tsx diff --git a/config/jest.config.js b/config/jest.config.js index 1f463b9d7..a6aa3d147 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -22,6 +22,7 @@ export default { '@fields/*': ['fields/*'], '@hooks/*': ['hooks/*'], '@libs/*': ['libs/*'], + '@tables/*': ['tables/*'], '@types_/*': ['types/*'], '@utils/*': ['utils/*'] }, diff --git a/src/tables/SimpleTable/CellLoader.tsx b/src/tables/SimpleTable/CellLoader.tsx new file mode 100644 index 000000000..a3760dc38 --- /dev/null +++ b/src/tables/SimpleTable/CellLoader.tsx @@ -0,0 +1,29 @@ +import styled, { keyframes } from 'styled-components' + +const cellLoaderAnimation = keyframes` + from { + left: -100%; + } + + to { + left: 100%; + } +` +export const CellLoader = styled.div` + background: ${p => p.theme.color.gainsboro}; + height: 18px; + overflow: hidden; + position: relative; + + &:before { + animation: ${cellLoaderAnimation} 1s cubic-bezier(0.4, 0, 0.2, 1) infinite; + background: linear-gradient(to right, transparent 0%, ${p => p.theme.color.white} 50%, transparent 100%); + content: ''; + display: block; + height: 100%; + left: -100%; + position: absolute; + top: 0; + width: 100%; + } +` diff --git a/src/tables/SimpleTable/Td.tsx b/src/tables/SimpleTable/Td.tsx new file mode 100644 index 000000000..7bdcab14f --- /dev/null +++ b/src/tables/SimpleTable/Td.tsx @@ -0,0 +1,25 @@ +import styled from 'styled-components' + +import { CellLoader } from './CellLoader' + +import type { TdHTMLAttributes } from 'react' + +type TdProps = TdHTMLAttributes & { + $isCenter?: boolean | undefined + $isLoading?: boolean | undefined +} +export const Td = styled.td.attrs(props => ({ + children: props.$isLoading ? : props.children +}))` + border-bottom: 1px solid ${p => p.theme.color.lightGray}; + border-right: 1px solid ${p => p.theme.color.lightGray}; + color: ${p => p.theme.color.gunMetal}; + font-size: 13px; + font-weight: 500; + max-width: 0; + overflow: hidden; + padding: 10px; + text-align: ${p => (p.$isCenter ? 'center' : 'left')}; + text-overflow: ellipsis; + white-space: nowrap; +` diff --git a/src/tables/SimpleTable.tsx b/src/tables/SimpleTable/index.tsx similarity index 73% rename from src/tables/SimpleTable.tsx rename to src/tables/SimpleTable/index.tsx index 375e51d99..99dc0ed7e 100644 --- a/src/tables/SimpleTable.tsx +++ b/src/tables/SimpleTable/index.tsx @@ -1,37 +1,32 @@ import classnames from 'classnames' import styled from 'styled-components' +import { CellLoader } from './CellLoader' +import { Td } from './Td' + import type { TableHTMLAttributes } from 'react' const Table = styled.table.attrs>(props => ({ className: classnames('Table-SimpleTable', props.className) }))` - width: 100%; - table-layout: auto; - overflow: auto; border-collapse: separate; + overflow: auto; + table-layout: auto; ` + const Head = styled.thead` position: sticky; top: 0; z-index: 1; - th:first-child { + > th:first-child { border-left: 1px solid ${p => p.theme.color.lightGray}; } ` -const SortContainer = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - cursor: default; - - &.cursor-pointer { - cursor: pointer; - } -` -const Th = styled.th` +const Th = styled.th<{ + $width?: number | undefined +}>` background-color: ${p => p.theme.color.gainsboro}; border-top: 1px solid ${p => p.theme.color.lightGray}; border-bottom: 1px solid ${p => p.theme.color.lightGray}; @@ -42,35 +37,35 @@ const Th = styled.th` padding: 10px; overflow: hidden; text-overflow: ellipsis; + ${p => !!p.$width && `width: ${p.$width}px;`} white-space: nowrap; ` +const SortContainer = styled.div` + align-items: center; + cursor: default; + display: flex; + justify-content: space-between; + + &.cursor-pointer { + cursor: pointer; + } +` + const BodyTr = styled.tr` - :hover { + &:hover { > td { background-color: ${p => p.theme.color.blueYonder25}; } } - td:first-child { + > td:first-child { border-left: 1px solid ${p => p.theme.color.lightGray}; } ` -const Td = styled.td<{ $isCenter?: boolean }>` - font-size: 13px; - font-weight: 500; - color: ${p => p.theme.color.gunMetal}; - text-align: ${p => (p.$isCenter ? 'center' : 'left')}; - border-bottom: 1px solid ${p => p.theme.color.lightGray}; - border-right: 1px solid ${p => p.theme.color.lightGray}; - overflow: hidden; - padding: 10px; - text-overflow: ellipsis; - white-space: nowrap; -` - export const SimpleTable = { BodyTr, + CellLoader, Head, SortContainer, Table, diff --git a/src/tables/TableWithSelectableRows/index.tsx b/src/tables/TableWithSelectableRows/index.tsx index 5752709f1..a3d567f48 100644 --- a/src/tables/TableWithSelectableRows/index.tsx +++ b/src/tables/TableWithSelectableRows/index.tsx @@ -3,58 +3,64 @@ import styled from 'styled-components' import { RowCheckbox } from './RowCheckbox' import { SimpleTable } from '../SimpleTable' -export { RowCheckbox } - const Table = styled(SimpleTable.Table)` border-collapse: separate; border-spacing: 0 5px; table-layout: fixed; ` + const Head = styled(SimpleTable.Head)` - th:last-child { + > th:last-child { border-right: 1px solid ${p => p.theme.color.lightGray}; } ` -const SortContainer = styled(SimpleTable.SortContainer)` - justify-content: start; - gap: 8px; -` -const Th = styled(SimpleTable.Th)<{ $width: number }>` +const Th = styled(SimpleTable.Th)` background-color: ${p => p.theme.color.white}; border-top: 1px solid ${p => p.theme.color.lightGray}; border-bottom: 1px solid ${p => p.theme.color.lightGray}; border-right: none; padding: 2px 16px; - width: ${p => p.$width}px; +` + +const SortContainer = styled(SimpleTable.SortContainer)` + gap: 8px; + justify-content: start; ` const BodyTr = styled(SimpleTable.BodyTr)<{ $isHighlighted?: boolean }>` - td:first-child { + > td:first-child { border-left: ${p => p.$isHighlighted ? `2px solid ${p.theme.color.blueGray}` : `1px solid ${p.theme.color.lightGray}`}; } - td:last-child { + > td:last-child { border-right: ${p => p.$isHighlighted ? `2px solid ${p.theme.color.blueGray}` : `1px solid ${p.theme.color.lightGray}`}; overflow: visible; } ` -const Td = styled(SimpleTable.Td)<{ $hasRightBorder: boolean; $isHighlighted?: boolean; $width: number }>` +const Td = styled(SimpleTable.Td)<{ + $hasRightBorder: boolean + $isHighlighted?: boolean + // TODO This should be removed, a table column width should only be set via its `th` width. + /** @deprecated Will be removed in the next major version. Use `Td.$width` instead to set columns width. */ + $width?: number | undefined +}>` background-color: ${p => p.theme.color.cultured}; - border-top: ${p => - p.$isHighlighted ? `2px solid ${p.theme.color.blueGray}` : `1px solid ${p.theme.color.lightGray}`}; border-bottom: ${p => p.$isHighlighted ? `2px solid ${p.theme.color.blueGray}` : `1px solid ${p.theme.color.lightGray}`}; border-right: none; - padding: 4px 16px; border-right: ${p => (p.$hasRightBorder ? `1px solid ${p.theme.color.lightGray}` : '')}; + border-top: ${p => + p.$isHighlighted ? `2px solid ${p.theme.color.blueGray}` : `1px solid ${p.theme.color.lightGray}`}; + padding: 4px 16px; width: ${p => p.$width}px; ` export const TableWithSelectableRows = { BodyTr, + CellLoader: SimpleTable.CellLoader, Head, RowCheckbox, SortContainer, diff --git a/stories/fields/CheckPicker/WithCustomSearch.stories.tsx b/stories/fields/CheckPicker/WithCustomSearch.stories.tsx index 582c88d85..01c753c98 100644 --- a/stories/fields/CheckPicker/WithCustomSearch.stories.tsx +++ b/stories/fields/CheckPicker/WithCustomSearch.stories.tsx @@ -57,7 +57,7 @@ const meta: Meta> = { export default meta -export function _CheckPickerWithCustomSearch(props: CheckPickerProps) { +export function WithCustomSearch(props: CheckPickerProps) { const optionsRef = useRef( (SPECIES as Specy[]).map(specy => ({ label: `${specy.code} - ${specy.name}`, diff --git a/stories/fields/MultiCascader/WithThreeColumns.stories.tsx b/stories/fields/MultiCascader/WithThreeColumns.stories.tsx index 271aa909b..f6f89a68e 100644 --- a/stories/fields/MultiCascader/WithThreeColumns.stories.tsx +++ b/stories/fields/MultiCascader/WithThreeColumns.stories.tsx @@ -30,7 +30,7 @@ const args: MultiCascaderProps = { /* eslint-disable sort-keys-fix/sort-keys-fix */ const meta: Meta> = { title: 'Fields/MultiCascader (variations)', - component: MultiCascaderWithThreeColumns, + component: MultiCascader, argTypes: { value: { @@ -53,7 +53,7 @@ const meta: Meta> = { export default meta -export function MultiCascaderWithThreeColumns(props: MultiCascaderProps) { +export function WithThreeColumns(props: MultiCascaderProps) { const [outputValue, setOutputValue] = useState(props.value ?? '∅') const { controlledOnChange, controlledValue } = useFieldControl(props.value, setOutputValue) diff --git a/stories/fields/MultiSelect/WithCustomSearch.stories.tsx b/stories/fields/MultiSelect/WithCustomSearch.stories.tsx index 56a0ba09d..edf58cfc5 100644 --- a/stories/fields/MultiSelect/WithCustomSearch.stories.tsx +++ b/stories/fields/MultiSelect/WithCustomSearch.stories.tsx @@ -52,7 +52,7 @@ const meta: Meta> = { export default meta -export function MultiSelectWithCustomSearch(props: MultiSelectProps) { +export function WithCustomSearch(props: MultiSelectProps) { const optionsRef = useRef( (SPECIES as Specy[]).map(specy => ({ label: `${specy.code} - ${specy.name}`, diff --git a/stories/fields/Select/WithCustomSearch.stories.tsx b/stories/fields/Select/WithCustomSearch.stories.tsx index 2ac7a52f9..6a04fe786 100644 --- a/stories/fields/Select/WithCustomSearch.stories.tsx +++ b/stories/fields/Select/WithCustomSearch.stories.tsx @@ -51,7 +51,7 @@ const meta: Meta> = { export default meta -export function SelectWithCustomSearch(props: SelectProps) { +export function WithCustomSearch(props: SelectProps) { const optionsRef = useRef( (SPECIES as Specy[]).map(specy => ({ label: `${specy.code} - ${specy.name}`, diff --git a/stories/tables/SimpleTable/WithLoader.stories.tsx b/stories/tables/SimpleTable/WithLoader.stories.tsx new file mode 100644 index 000000000..50296b46d --- /dev/null +++ b/stories/tables/SimpleTable/WithLoader.stories.tsx @@ -0,0 +1,70 @@ +import ky from 'ky' +import { useEffect, useState } from 'react' + +import { META_DEFAULTS } from '../../../.storybook/constants' +import { generateStoryDecorator } from '../../../.storybook/utils/generateStoryDecorator' +import { SimpleTable } from '../../../src' + +import type { Meta } from '@storybook/react' + +/* eslint-disable sort-keys-fix/sort-keys-fix */ +const meta: Meta<{}> = { + ...META_DEFAULTS, + + title: 'Tables/SimpleTable (variations)', + + decorators: [ + generateStoryDecorator({ + withBackgroundButton: true + }) + ] +} +/* eslint-enable sort-keys-fix/sort-keys-fix */ + +export default meta + +export function WithLoader() { + const [data, setData] = useState(undefined) + + const emptyRows = new Array(10).fill(undefined) + const isLoading = !data + + useEffect(() => { + const timer = setTimeout(async () => { + const nextData: any = await ky.get('https://api.openbrewerydb.org/v1/breweries?per_page=10').json() + + setData(nextData) + }, 5000) + + return () => clearTimeout(timer) + }, []) + + return ( + + + ID + Name + Address + + + {isLoading && + emptyRows.map((_, index) => ( + // eslint-disable-next-line react/no-array-index-key + + + + + + ))} + {!isLoading && + data?.map(brewery => ( + + {brewery.id} + {brewery.name} + {`${brewery.street}, ${brewery.city} ${brewery.postalCode}, ${brewery.state}`} + + ))} + + + ) +} diff --git a/tsconfig.json b/tsconfig.json index 4eb9a585f..12b35cbd6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,7 @@ "@fields/*": ["fields/*"], "@hooks/*": ["hooks/*"], "@libs/*": ["libs/*"], + "@tables/*": ["tables/*"], "@types_/*": ["types/*"], "@utils/*": ["utils/*"] },