-
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
312 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,172 @@ | ||
import styled, { css } from 'styled-components'; | ||
|
||
const maxCols = 12; | ||
|
||
interface GapProps { | ||
gap?: number; | ||
} | ||
|
||
interface AlignProps { | ||
align: 'start' | 'center' | 'end' | 'baseline'; | ||
} | ||
|
||
interface JustifyProps { | ||
justify: 'start' | 'center' | 'end' | 'between' | 'around' | 'baseline'; | ||
} | ||
|
||
export type TableCellProps = | ||
| { | ||
col: number; | ||
min?: never; | ||
width?: never; | ||
} | ||
| { | ||
col?: never; | ||
min: boolean; | ||
width?: never; | ||
} | ||
| { | ||
col?: never; | ||
min?: never; | ||
width?: never; | ||
} | ||
| { | ||
col?: never; | ||
min?: never; | ||
width: number | string; | ||
}; | ||
|
||
export interface TableProps extends GapProps { | ||
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', | ||
}; | ||
|
||
/** | ||
* Table wrapper | ||
* | ||
* @param width Optional | ||
* @param gap Optional offset between child elements | ||
*/ | ||
export const Table = styled.div<TableProps>` | ||
display: flex; | ||
flex-direction: column; | ||
${({ width }) => | ||
width && | ||
css` | ||
width: ${width}px; | ||
`} | ||
${({ gap }) => | ||
gap && | ||
css` | ||
gap: ${gap}px; | ||
`} | ||
`; | ||
|
||
/** | ||
* TableRow wrapper | ||
* | ||
* @param align As `align-items` for flexible row container | ||
* @param justify As `justify-content` for flexible row container | ||
*/ | ||
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; | ||
`} | ||
`; | ||
|
||
/** | ||
* TableCell component | ||
* | ||
* @param col Size of column width in range from `1` to `12` | ||
* @param min Minimum size column, use instead of `col` prop | ||
* @param justify align content as `justify-content` in flexible container | ||
*/ | ||
|
||
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> | ||
); | ||
}; |