-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Table): implements flexible table component
- Loading branch information
Showing
3 changed files
with
299 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import styled, { css } from 'styled-components'; | ||
|
||
const maxCols = 12; | ||
|
||
interface GapProps { | ||
/** Offset between child elements */ | ||
gap?: number; | ||
} | ||
|
||
interface AlignProps { | ||
/** As `align-items` for flexible row container */ | ||
align: 'start' | 'center' | 'end' | 'baseline'; | ||
} | ||
|
||
interface JustifyProps { | ||
/** As `justify-content` for flexible container */ | ||
justify: 'start' | 'center' | 'end' | 'between' | 'around' | 'baseline'; | ||
} | ||
|
||
export type TableCellProps = | ||
| { | ||
/** Size of column width in range from `1` to `12` */ | ||
col: number; | ||
/** Minimum size column, use instead of `col` prop */ | ||
min?: never; | ||
/** Absolute or relative width, ex. `10ch` or `100` */ | ||
width?: never; | ||
} | ||
| { | ||
col?: never; | ||
min: boolean; | ||
width?: never; | ||
} | ||
| { | ||
col?: never; | ||
min?: never; | ||
width: number | string; | ||
} | ||
| { | ||
col?: never; | ||
min?: never; | ||
width?: never; | ||
}; | ||
|
||
export interface TableProps extends GapProps { | ||
/** Container width */ | ||
width?: number; | ||
} | ||
|
||
const mapRuleSet: { [key in AlignProps['align'] | JustifyProps['justify']]: string } = { | ||
baseline: 'baseline', | ||
start: 'flex-start', | ||
end: 'flex-end', | ||
center: 'center', | ||
around: 'space-around', | ||
between: 'space-between', | ||
}; | ||
|
||
export const Table = styled.div<TableProps>` | ||
display: flex; | ||
flex-direction: column; | ||
${({ width }) => | ||
width && | ||
css` | ||
width: ${width}px; | ||
`} | ||
${({ gap }) => | ||
gap && | ||
css` | ||
gap: ${gap}px; | ||
`} | ||
`; | ||
|
||
export const TableRow = styled.div<Partial<AlignProps & JustifyProps> & GapProps>` | ||
display: flex; | ||
flex-basis: 100%; | ||
align-items: flex-start; | ||
justify-content: flex-start; | ||
${({ align }) => | ||
align && | ||
css` | ||
align-items: ${mapRuleSet[align]}; | ||
`} | ||
${({ justify }) => | ||
justify && | ||
css` | ||
justify-content: ${mapRuleSet[justify]}; | ||
`} | ||
${({ gap }) => | ||
gap && | ||
css` | ||
gap: ${gap}px; | ||
`} | ||
`; | ||
|
||
export const TableCell = styled.div<TableCellProps & Partial<JustifyProps & AlignProps>>` | ||
display: inline-flex; | ||
align-items: baseline; | ||
flex-wrap: wrap; | ||
${({ col }) => { | ||
if (!col) { | ||
return css` | ||
flex: 1 0 0%; | ||
max-width: 100%; | ||
`; | ||
} | ||
if (col > maxCols) { | ||
return css` | ||
flex: 0 0 100%; | ||
max-width: 100%; | ||
`; | ||
} | ||
const width = (col / maxCols) * 100; | ||
return css` | ||
flex: 0 0 ${width}%; | ||
max-width: {width}%; | ||
`; | ||
}} | ||
${({ width }) => { | ||
if (width == null) { | ||
return; | ||
} | ||
const unit = typeof width === 'number' ? 'px' : ''; | ||
return css` | ||
flex: 0 0 ${width}${unit}; | ||
max-width: ${width}${unit}; | ||
`; | ||
}} | ||
${({ min }) => | ||
min && | ||
css` | ||
flex-basis: 0%; | ||
max-width: max-content; | ||
`} | ||
${({ justify }) => | ||
justify && | ||
css` | ||
justify-content: ${mapRuleSet[justify]}; | ||
`} | ||
${({ align }) => | ||
align && | ||
css` | ||
align-self: ${mapRuleSet[align]}; | ||
`} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import React, { useState } from 'react'; | ||
import type { Meta, StoryFn } from '@storybook/react'; | ||
import { gray5, gray3 } from '@taskany/colors'; | ||
import styled from 'styled-components'; | ||
|
||
import { Table, TableRow, TableCell } from '../components/Table'; | ||
import { GoalIcon } from '../components/Icon/GoalIcon'; | ||
import { Text } from '../components/Text'; | ||
import { UserPic } from '../components/UserPic'; | ||
import { Dot } from '../components/Dot'; | ||
import { Tag } from '../components/Tag'; | ||
import { CircleProgressBar } from '../components/CircleProgressBar'; | ||
import { Button } from '../components/Button'; | ||
|
||
const meta: Meta<typeof Table> = { | ||
title: 'Table', | ||
component: Table, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryFn<typeof meta>; | ||
|
||
const data = Array.from({ length: 10 }, (_, i) => ({ | ||
title: `Title ${i + 1}`, | ||
projectId: (Math.random() * 10).toString(16).slice(2, 8).padEnd(6, 'AB').toUpperCase(), | ||
tags: Array.from({ length: Math.ceil(Math.random() * 5) }, (_, i) => `Tag ${i + 1}`), | ||
progress: Math.round(Math.random() * 100), | ||
children: Array.from({ length: 3 }, (_, i) => ({ | ||
title: `Title ${i + 1}`, | ||
projectId: (Math.random() * 10).toString(16).slice(2, 8).padEnd(6, 'AB').toUpperCase(), | ||
tags: Array.from({ length: Math.ceil(Math.random() * 5) }, (_, i) => `Tag ${i + 1}`), | ||
progress: Math.round(Math.random() * 100), | ||
children: Array.from({ length: 3 }, (_, i) => ({ | ||
title: `Title ${i + 1}`, | ||
projectId: (Math.random() * 10).toString(16).slice(2, 8).padEnd(6, 'AB').toUpperCase(), | ||
tags: Array.from({ length: Math.ceil(Math.random() * 5) }, (_, i) => `Tag ${i + 1}`), | ||
progress: Math.round(Math.random() * 100), | ||
children: undefined, | ||
})), | ||
})), | ||
})); | ||
|
||
const StyledTableRow = styled(TableRow)<{ depth?: number }>` | ||
padding: 5px 0; | ||
border-bottom: 1px solid ${gray5}; | ||
border-top: 1px solid ${gray3}; | ||
&:first-child { | ||
border-top: 0; | ||
} | ||
&:last-child { | ||
border-bottom: 0; | ||
} | ||
& > ${TableCell}:first-child { | ||
padding-left: calc(1rem * ${({ depth = 0 }) => depth}); | ||
box-sizing: border-box; | ||
} | ||
`; | ||
|
||
export const Default: Story = () => { | ||
return ( | ||
<Table width={600}> | ||
{data.map(({ title, projectId, tags, progress }) => ( | ||
<StyledTableRow key={title} align="center" gap={10}> | ||
<TableCell min> | ||
<GoalIcon size="xxs" noWrap /> | ||
</TableCell> | ||
<TableCell col={5}> | ||
<Text size="s" weight="bold"> | ||
<Dot size="m" view="primary" /> | ||
{title} | ||
</Text> | ||
</TableCell> | ||
<TableCell min> | ||
<CircleProgressBar value={progress} size="s" /> | ||
</TableCell> | ||
<TableCell width="6ch"> | ||
<Text size="s" weight="thin"> | ||
{projectId} | ||
</Text> | ||
</TableCell> | ||
<TableCell justify="end"> | ||
{tags.map((t) => ( | ||
<Tag size="s" title={t} /> | ||
))} | ||
</TableCell> | ||
<TableCell min justify="center"> | ||
<UserPic size={14} email="admin@taskany.org" /> | ||
</TableCell> | ||
</StyledTableRow> | ||
))} | ||
</Table> | ||
); | ||
}; | ||
|
||
const CollapseRow = ({ rowData, childData, depth = 0 }) => { | ||
const [showChilds, setShowChilds] = useState(false); | ||
return ( | ||
<> | ||
<StyledTableRow depth={depth}> | ||
<TableCell col={5}> | ||
<Text size="s" weight="bold"> | ||
<Dot size="m" view="primary" /> | ||
{rowData.title} | ||
</Text> | ||
</TableCell> | ||
<TableCell min> | ||
<CircleProgressBar value={rowData.progress} size="s" /> | ||
</TableCell> | ||
{childData?.length ? ( | ||
<TableCell justify="center"> | ||
<Button | ||
size="s" | ||
ghost | ||
text={showChilds ? 'hide' : 'show'} | ||
onClick={() => setShowChilds((prev) => !prev)} | ||
/> | ||
</TableCell> | ||
) : null} | ||
</StyledTableRow> | ||
{showChilds && | ||
childData?.map(({ title, progress, children }) => ( | ||
<CollapseRow rowData={{ title, progress }} childData={children} key={title} depth={depth + 1} /> | ||
))} | ||
</> | ||
); | ||
}; | ||
|
||
export const RecursiveTable = () => { | ||
return ( | ||
<Table width={600}> | ||
{data.slice(0, 3).map(({ title, progress, children }) => ( | ||
<CollapseRow rowData={{ title, progress }} childData={children} key={title} /> | ||
))} | ||
</Table> | ||
); | ||
}; |