Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dony] headless Table 구현 #95

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/components/common/Table/BasicTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ReactElement, ReactNode } from 'react';

import { offscreen } from '@styles/commonStyles';

export type BasicTableProps = {
caption: string;
columnsWidth?: string[];
children: (ReactElement | ReactElement[])[];
};

const BasicTable = ({
caption,
columnsWidth,
children,
...restProps
}: BasicTableProps) => (
<table {...restProps}>
<caption css={offscreen}>{caption}</caption>
{columnsWidth && (
<colgroup>
{columnsWidth.map((width, index) => (
// 유동적으로 변하지 않는 리스트
// eslint-disable-next-line react/no-array-index-key
<col key={index} width={width} />
))}
</colgroup>
)}
<tbody>{children}</tbody>
</table>
);

type RowProps = {
children: ReactElement | ReactElement[];
};

const Row = ({ children, ...restProps }: RowProps) => (
<tr {...restProps}>{children}</tr>
);

type CellProps = {
colSpan?: number;
rowSpan?: number;
children: ReactNode;
};

const Cell = ({ children, ...restProps }: CellProps) => (
<td {...restProps}>{children}</td>
);

BasicTable.Row = Row;
BasicTable.Cell = Cell;

export default BasicTable;
106 changes: 106 additions & 0 deletions src/components/common/Table/Cell/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { ReactNode, Dispatch, SetStateAction } from 'react';

import Button from '@components/common/Button';

const DIRECTION = {
ASCENDING: 'ascending',
DESCENDING: 'descending',
};

export type SortState = {
name: string;
direction: typeof DIRECTION[keyof typeof DIRECTION];
sortIndices: number[];
} | null;

export type SortConfig = {
sortValues?: { [key in string]: (string | number)[] };
sortState: SortState;
setSortState: Dispatch<SetStateAction<SortState>>;
};
Comment on lines +16 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별 건 아니지만 문득 도니가 사용하신 [key in string]과 [key: string]이 무슨 차이인지 궁금해서 찾아봤었는데요! 제 생각엔 [key: string] 쪽이 좀 더 적절하지 않나 싶었던 것 같아요.
관련해서 정리한 링크입니다!

그리고 sortState가 현재 어떤 것을 기준으로 정렬하고 있는지에 관한 정보니까 이런 부분을 좀 더 명확히 나타낼 수 있는 이름이면 더 좋겠다는 생각도 드네요!


type CellProps = {
colSpan?: number;
rowSpan?: number;
name?: string;
sortConfig?: SortConfig;
children: ReactNode;
};

interface HeadCellProps extends CellProps {
scope?: 'col' | 'row';
sortConfig: SortConfig;
}

const HeadCell = ({
name = '',
scope,
sortConfig,
children,
...restProps
}: HeadCellProps) => {
const { sortValues = {}, sortState, setSortState } = sortConfig;
const direction =
sortState &&
sortState.name === name &&
sortState.direction === DIRECTION.DESCENDING
? DIRECTION.ASCENDING
: DIRECTION.DESCENDING;

const getSortIndices = () => {
const sortValuesByName = sortValues[name].map((value, index) => ({
value,
index,
}));
sortValuesByName.sort((a, b) => {
if (a.value > b.value) return direction === DIRECTION.ASCENDING ? 1 : -1;
if (a.value < b.value) return direction === DIRECTION.ASCENDING ? -1 : 1;
return 0;
});

return sortValuesByName.map(({ index }) => index);
};
Comment on lines +50 to +62
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateSortState를 실행할 때 매번 이 연산을 수행하게 되는데 같은 컬럼을 여러번 클릭하는 경우에는 매번 계산하는 게 불필요할 수 있을 것 같아요! 현재 sortState에 있는 name과 같다면 가지고 있는 sortIndices를 reverse해서 반환해줘도 되지 않을까요?


const updateSortState = () => {
if (!sortValues[name])
throw new Error('Please set sortValues[name] in Table props.');

const newSortState = {
name,
direction,
sortIndices: getSortIndices(),
};
setSortState(newSortState);
};

return (
<th
scope={scope}
data-sort={sortState?.name === name ? sortState?.direction : undefined}
{...restProps}
>
{name ? (
<Button type="button" onClick={updateSortState}>
{children}
</Button>
) : (
children
)}
</th>
);
};

const BodyCell = ({ children, ...restProps }: CellProps) => (
<td {...restProps}>{children}</td>
);

const Cell = ({ name, sortConfig, children, ...restProps }: CellProps) =>
sortConfig ? (
<HeadCell sortConfig={sortConfig} name={name} {...restProps}>
{children}
</HeadCell>
) : (
<BodyCell {...restProps}>{children}</BodyCell>
);

export default Cell;
68 changes: 68 additions & 0 deletions src/components/common/Table/ColumnTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { cloneElement, ReactElement, useState } from 'react';

import Cell, { SortState, SortConfig } from '@components/common/Table/Cell';
import { offscreen } from '@styles/commonStyles';

export type ColumnTableProps = {
caption: string;
columnsWidth?: string[];
sortValues?: { [key in string]: (string | number)[] };
children: (ReactElement | ReactElement[])[];
};

const ColumnTable = ({
caption,
columnsWidth,
sortValues,
children,
...restProps
}: ColumnTableProps) => {
const Children = React.Children.toArray(children);
const [headRow, bodyRows] = [Children[0], Children.splice(1)];
const [sortState, setSortState] = useState<SortState>(null);
const sortConfig = { sortValues, sortState, setSortState };

return (
<table {...restProps}>
<caption css={offscreen}>{caption}</caption>
{columnsWidth && (
<colgroup>
{columnsWidth.map((width, index) => (
// 유동적으로 변하지 않는 리스트
// eslint-disable-next-line react/no-array-index-key
<col key={index} width={width} />
))}
</colgroup>
)}
<thead>
{cloneElement(headRow as ReactElement, { scope: 'col', sortConfig })}
</thead>
<tbody>
{sortState
? sortState.sortIndices.map((index) => bodyRows[index])
: bodyRows}
</tbody>
</table>
);
};

type RowProps = {
scope?: 'col';
sortConfig?: SortConfig;
children: ReactElement | ReactElement[];
};

const Row = ({ scope, sortConfig, children, ...restProps }: RowProps) => (
<tr {...restProps}>
{sortConfig
? React.Children.toArray(children).map((child) =>
cloneElement(child as ReactElement, { scope, sortConfig }),
)
: children}
</tr>
);

ColumnTable.Row = Row;
ColumnTable.Cell = Cell;

export default ColumnTable;
67 changes: 67 additions & 0 deletions src/components/common/Table/RowTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { cloneElement, ReactElement, useState } from 'react';

import Cell, { SortState, SortConfig } from '@components/common/Table/Cell';
import { offscreen } from '@styles/commonStyles';

export type RowTableProps = {
caption: string;
columnsWidth?: string[];
sortValues?: { [key in string]: (string | number)[] };
children: ReactElement | ReactElement[];
};

const RowTable = ({
caption,
columnsWidth,
sortValues,
children,
...restProps
}: RowTableProps) => {
const [sortState, setSortState] = useState<SortState>(null);
const sortConfig = { sortValues, sortState, setSortState };

return (
<table {...restProps}>
<caption css={offscreen}>{caption}</caption>
{columnsWidth && (
<colgroup>
{columnsWidth.map((width, index) => (
// 유동적으로 변하지 않는 리스트
// eslint-disable-next-line react/no-array-index-key
<col key={index} width={width} />
))}
</colgroup>
)}
<tbody>
{React.Children.toArray(children).map((child) =>
cloneElement(child as ReactElement, { scope: 'row', sortConfig }),
)}
</tbody>
</table>
);
};

type RowProps = {
scope?: 'row';
sortConfig?: SortConfig;
children: (ReactElement | ReactElement[])[];
};

const Row = ({ scope, sortConfig, children, ...restProps }: RowProps) => {
const Children = React.Children.toArray(children);
const [headCell, bodyCells] = [Children[0], Children.splice(1)];

return (
<tr {...restProps}>
{cloneElement(headCell as ReactElement, { scope, sortConfig })}
{sortConfig?.sortState
? sortConfig.sortState.sortIndices.map((index) => bodyCells[index])
: bodyCells}
</tr>
);
};

RowTable.Row = Row;
RowTable.Cell = Cell;

export default RowTable;
5 changes: 5 additions & 0 deletions src/components/common/Table/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import BasicTable from '@components/common/Table/BasicTable';
import ColumnTable from '@components/common/Table/ColumnTable';
import RowTable from '@components/common/Table/RowTable';

export { BasicTable, ColumnTable, RowTable };