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

feat(Descriptions): support Descriptions component #2706

Merged
merged 10 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 26 additions & 18 deletions script/generate-usage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -1062,24 +1062,6 @@ module.exports = {
}, [changedProps]);
`,
},
Jumper: {
importStr: `
import configProps from './props.json';\n
import { Jumper } from 'tdesign-react';\n`,
configStr: `
const [configList, setConfigList] = useState(configProps);
`,
panelStr: `
const panelList = [{ label: 'jumper', value: 'jumper' }];
`,
usageStr: `
useEffect(() => {
setRenderComp((
<Jumper {...changedProps}></Jumper>
));
}, [changedProps]);
`,
},
Collapse: {
importStr: `
import configProps from './props.json';\n
Expand Down Expand Up @@ -1113,4 +1095,30 @@ module.exports = {
}, [changedProps]);
`,
},
Descriptions: {
importStr: `
import configProps from './props.json';\n
import { Descriptions } from 'tdesign-react';\n`,
configStr: `
const [configList, setConfigList] = useState(configProps);
`,
panelStr: `
const panelList = [{ label: 'descriptions', value: 'descriptions' }];
`,
usageStr: `
const { DescriptionsItem } = Descriptions;
useEffect(() => {
setRenderComp((
<Descriptions title="Shipping address" {...changedProps}>
<DescriptionsItem label="Name">TDesign</DescriptionsItem>
<DescriptionsItem label="Telephone Number">139****0609</DescriptionsItem>
<DescriptionsItem label="Area">China Tencent Headquarters</DescriptionsItem>
<DescriptionsItem label="Address" content="test">
Shenzhen Penguin Island D1 4A Mail Center
</DescriptionsItem>
</Descriptions>
));
}, [changedProps]);
`,
},
};
8 changes: 8 additions & 0 deletions site/site.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,14 @@ export const docs = [
component: () => import('tdesign-react/comment/comment.md'),
componentEn: () => import('tdesign-react/comment/comment.en-US.md'),
},
{
title: 'Descriptions 描述',
titleEn: 'Descriptions',
name: 'descriptions',
path: '/react/components/descriptions',
component: () => import('tdesign-react/descriptions/descriptions.md'),
componentEn: () => import('tdesign-react/descriptions/descriptions.en-US.md'),
},
{
title: 'Image 图片',
titleEn: 'Image',
Expand Down
8 changes: 7 additions & 1 deletion site/test-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ module.exports = {
"functions": "60%",
"lines": "66.07%"
},
"descriptions": {
"statements": "98.82%",
"branches": "100%",
"functions": "95.45%",
"lines": "100%"
},
"dialog": {
"statements": "85%",
"branches": "71.42%",
Expand Down Expand Up @@ -403,7 +409,7 @@ module.exports = {
},
"treeSelect": {
"statements": "95.45%",
"branches": "85.95%",
"branches": "86.17%",
"functions": "97.61%",
"lines": "97.2%"
},
Expand Down
2 changes: 1 addition & 1 deletion src/collapse/_usage/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import BaseUsage, {
} from "@site/src/components/BaseUsage";
import jsxToString from "react-element-to-jsx-string";

import { Collapse } from "tdesign-react";
import configProps from "./props.json";

import { Collapse } from "tdesign-react";

export default function Usage() {
const [configList, setConfigList] = useState(configProps);
Expand Down
5 changes: 5 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export type HorizontalAlignEnum = 'left' | 'center' | 'right';

export type VerticalAlignEnum = 'top' | 'middle' | 'bottom';

export enum LayoutEnum {
VERTICAL = 'vertical',
HORIZONTAL = 'horizontal',
}

export type ClassName = { [className: string]: any } | ClassName[] | string;

export type CSSSelector = string;
Expand Down
146 changes: 146 additions & 0 deletions src/descriptions/Descriptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React from 'react';
import classNames from 'classnames';
import isArray from 'lodash/isArray';
import assign from 'lodash/assign';
import { TdDescriptionItemProps, TdDescriptionsProps } from './type';
import { descriptionItemDefaultProps, descriptionsDefaultProps } from './defaultProps';
import useDefaultProps from '../hooks/useDefaultProps';
import useConfig from '../hooks/useConfig';
import useCommonClassName from '../hooks/useCommonClassName';
import { LayoutEnum } from '../common';
import { DescriptionsContext } from './DescriptionsContext';
import DescriptionsItem from './DescriptionsItem';
import Row from './Row';

/**
* 实现思路
* 1. 基于 table tbody tr td 来实现布局
* 2. 通过 span 计算总共有几行以及每一行的 item 个数,特别注意最后一行,要填充满
* 3. 整体布局:左右布局(column 和 span 生效)/上下布局(column 和 span 失效,一行一个 item)
* 4. item 布局:左右布局/上下布局
*/

/**
* TDescriptions:承载 header(title) 和 body(table, tbody)
* TDescriptionsRow:承载每一行(tr)
* TDescriptionsItem:获取 item 数据(span, label, content)
*/

export type DescriptionsProps = TdDescriptionsProps & {
children?: React.ReactNode;
};

const Descriptions = (DescriptionsProps: DescriptionsProps) => {
const props = useDefaultProps<DescriptionsProps>(DescriptionsProps, descriptionsDefaultProps);

const { title, bordered, column, layout, items: rowItems, children } = props;

const { classPrefix } = useConfig();

const COMPONENT_NAME = `${classPrefix}-descriptions`;

const { SIZE } = useCommonClassName();

// 计算渲染的行内容
const getRows = () => {
// 1. 两种方式:a. props 传 items b. slots t-descriptions-item; a 优先级更高

let items: TdDescriptionItemProps[] = [];

if (isArray(rowItems)) {
/**
* 2.1 a 方式获取 items
* ! 这里要支持 label: string / <div></div> / () => <div></div>
* ! 暂时没有这样一个全局的方法,所以先在组件内部写一个临时方法,无论之后是有了更好的处理方式要删除掉,还是其它组件也需要时再放到公共方法里面,都是可行的
*/
items = rowItems.map((item) => {
const { span } = assign({}, descriptionItemDefaultProps, item);
return {
label: item.label,
content: item.content,
span,
};
});
} else {
// 2.2 b 方式 获取 TDescriptionsItem
const childrenList = React.Children.toArray(children).filter(
(child: JSX.Element) => child.type.displayName === DescriptionsItem.displayName,
);

if (childrenList.length !== 0) {
items = (childrenList as React.ReactElement[]).map(({ props: child }) => {
const { span } = assign({}, descriptionItemDefaultProps, child);

return {
label: child.label,
content: child.content ?? child.children,
span,
};
});
}
}

// 2. 判断布局,如果整体布局为 LayoutEnum.VERTICAL,那么直接返回即可。
if (layout === LayoutEnum.VERTICAL) {
return [items];
}
// 3. 布局为 LayoutEnum.HORIZONTAL 时,需要计算每一行的 item 个数
let temp: TdDescriptionItemProps[] = [];
let reset = column;
// 4. 记录结果
const res: TdDescriptionItemProps[][] = [];
items.forEach((item, index) => {
const { span } = item;
if (reset >= span) {
// 当前行还剩余空间
temp.push(item);
reset -= span;
} else {
// 当前行放不下了,放下一行
res.push(temp);
temp = [item];
reset = column - span;
}

if (index === items.length - 1) {
// 最后一个
Reflect.set(item, 'span', span + reset);
res.push(temp);
}
});

return res;
};

// Header
const renderHeader = () => (title ? <div className={`${COMPONENT_NAME}__header`}>{title}</div> : '');

// Body
const renderBody = () => {
const tableClass = [`${COMPONENT_NAME}__body`, SIZE[props.size], { [`${COMPONENT_NAME}__body--border`]: bordered }];
return (
<table className={classNames(tableClass)}>
<tbody>
{getRows().map((row, i) => (
<Row row={row} key={i} />
))}
</tbody>
</table>
);
};

return (
<DescriptionsContext.Provider value={props}>
<div className={COMPONENT_NAME}>
{renderHeader()}
{renderBody()}
</div>
</DescriptionsContext.Provider>
);
};

Descriptions.displayName = 'Descriptions';

Descriptions.DescriptionsItem = DescriptionsItem;

export default Descriptions;
6 changes: 6 additions & 0 deletions src/descriptions/DescriptionsContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createContext } from 'react';
import { TdDescriptionsProps } from './type';

export type DescriptionsContextProps = TdDescriptionsProps;

export const DescriptionsContext = createContext<DescriptionsContextProps>(null);
10 changes: 10 additions & 0 deletions src/descriptions/DescriptionsItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { TdDescriptionItemProps } from './type';

export type DescriptionsItem = TdDescriptionItemProps & { children?: React.ReactNode };

const DescriptionsItem: React.FC<DescriptionsItem> = () => null;

DescriptionsItem.displayName = 'DescriptionsItem';

export default DescriptionsItem;
108 changes: 108 additions & 0 deletions src/descriptions/Row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useContext } from 'react';
import { TdDescriptionItemProps } from './type';
import { LayoutEnum } from '../common';
import useConfig from '../hooks/useConfig';
import { DescriptionsContext } from './DescriptionsContext';

export type RowProps = { row: TdDescriptionItemProps[] };

const Row: React.FC<RowProps> = (props) => {
const { row } = props;

const { classPrefix } = useConfig();
const descriptionsContext = useContext(DescriptionsContext);

const COMPONENT_NAME = `${classPrefix}-descriptions`;

// label
const label = (node: TdDescriptionItemProps, layout: LayoutEnum = LayoutEnum.HORIZONTAL, rowKey?: string) => {
const { span } = node;
const labelSpan = layout === LayoutEnum.HORIZONTAL ? 1 : span;
return (
<td
key={rowKey}
colSpan={labelSpan}
className={`${COMPONENT_NAME}__label`}
style={descriptionsContext.labelStyle}
>
{node.label}
{descriptionsContext.colon && ':'}
</td>
);
};

// content
const content = (node: TdDescriptionItemProps, layout: LayoutEnum = LayoutEnum.HORIZONTAL, rowKey?: string) => {
const { span } = node;
const contentSpan = span > 1 && layout === LayoutEnum.HORIZONTAL ? span * 2 - 1 : span;
return (
<td
key={rowKey}
colSpan={contentSpan}
className={`${COMPONENT_NAME}__content`}
style={descriptionsContext.contentStyle}
>
{node.content}
</td>
);
};

// 总共有四种布局
// Layout horizontal vertical
// itemLayout horizontal vertical

const hh = () => (
<tr>
{row.map((node, i) => (
<React.Fragment key={i}>
{label(node)}
{content(node)}
</React.Fragment>
))}
</tr>
);

const hv = () => (
<>
<tr>{row.map((node, i) => label(node, LayoutEnum.VERTICAL, `top_${i}`))}</tr>
<tr>{row.map((node, i) => content(node, LayoutEnum.VERTICAL, `bottom_${i}`))}</tr>
</>
);

const vh = () => (
<>
{row.map((node, i) => (
<tr key={i}>
{label(node)}
{content(node)}
</tr>
))}
</>
);

const vv = () => (
<>
{row.map((node, i) => (
<React.Fragment key={i}>
<tr>{label(node)}</tr>
<tr>{content(node)}</tr>
</React.Fragment>
))}
</>
);

if (descriptionsContext.layout === LayoutEnum.HORIZONTAL) {
if (descriptionsContext.itemLayout === LayoutEnum.HORIZONTAL) {
return hh();
}
return hv();
}
if (descriptionsContext.itemLayout === LayoutEnum.HORIZONTAL) {
return vh();
}
return vv();
};

Row.displayName = 'DescriptionsRow';

export default Row;
Loading
Loading