Skip to content

Commit

Permalink
feat(Descriptions): support Descriptions component (#2706)
Browse files Browse the repository at this point in the history
* feat(support descriptions component): support Descriptions component

* test(descriptions): update snap

* docs(descriptions): update descriptions example

n

* test(descriptions): update test snap

n

* fix(descriptions): descriptions-item replace item

* docs: rename Descriptions direction to layout

* test: update snap test

* docs(descriptions): update usage

* docs(descriptions): update usage

---------

Co-authored-by: Heising <[email protected]>
  • Loading branch information
HaixingOoO and Heising authored Jan 11, 2024
1 parent d4a642e commit d164c94
Show file tree
Hide file tree
Showing 32 changed files with 4,010 additions and 72 deletions.
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

0 comments on commit d164c94

Please sign in to comment.