Skip to content

Commit

Permalink
feat(Table): implements flexible table component
Browse files Browse the repository at this point in the history
  • Loading branch information
LamaEats committed Jul 27, 2023
1 parent b213a79 commit 3ed7756
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 0 deletions.
172 changes: 172 additions & 0 deletions src/components/Table.tsx
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]};
`}
`;
1 change: 1 addition & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ export * from './FiltersContainers';
export * from './FiltersCounter';
export * from './FiltersDropdown';
export * from './UserMenuItem';
export * from './Table';
139 changes: 139 additions & 0 deletions src/stories/Table.stories.tsx
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>
);
};

0 comments on commit 3ed7756

Please sign in to comment.