feat: Descriptions items.span support responsive config (#44534)

* feat: Desc items support responsive

* refactor: useBreakPoint

* docs: update docs

* test: add test case

* chore: update def

* chore: fix def
This commit is contained in:
二货爱吃白萝卜 2023-08-30 22:09:32 +08:00 committed by GitHub
parent b0dd138fba
commit cbfb126690
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 200 additions and 104 deletions

View File

@ -1,4 +1,5 @@
import React from 'react';
import type { GlobalToken } from '../theme/interface';
import { useToken } from '../theme/internal';
@ -124,3 +125,14 @@ export default function useResponsiveObserver() {
};
}, [token]);
}
export const matchScreen = (screens: ScreenMap, screenSizes?: ScreenSizeMap) => {
if (screenSizes && typeof screenSizes === 'object') {
for (let i = 0; i < responsiveArray.length; i++) {
const breakpoint: Breakpoint = responsiveArray[i];
if (screens[breakpoint] && screenSizes[breakpoint] !== undefined) {
return screenSizes[breakpoint];
}
}
}
};

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import type { DescriptionsItemType } from '.';
import type { InternalDescriptionsItemType } from '.';
import Cell from './Cell';
import type { DescriptionsContextProps } from './DescriptionsContext';
import DescriptionsContext from './DescriptionsContext';
@ -12,7 +13,7 @@ interface CellConfig {
}
function renderCells(
items: DescriptionsItemType[],
items: InternalDescriptionsItemType[],
{ colon, prefixCls, bordered }: RowProps,
{
component,
@ -87,7 +88,7 @@ function renderCells(
export interface RowProps {
prefixCls: string;
vertical: boolean;
row: DescriptionsItemType[];
row: InternalDescriptionsItemType[];
bordered?: boolean;
colon: boolean;
index: number;

View File

@ -1116,13 +1116,32 @@ exports[`renders components/descriptions/demo/responsive.tsx extend context corr
Database version: 3.4
<br />
Package: dds.mongo.mid
</span>
</td>
</tr>
<tr
class="ant-descriptions-row"
>
<th
class="ant-descriptions-item-label"
colspan="1"
>
<span>
Hardware Info
</span>
</th>
<td
class="ant-descriptions-item-content"
colspan="1"
>
<span>
CPU: 6 Core 3.5 GHz
<br />
Storage space: 10 GB
<br />
Replication factor: 3
<br />
Region: East China 1
<br />
</span>
</td>
</tr>

View File

@ -1004,7 +1004,7 @@ exports[`renders components/descriptions/demo/responsive.tsx correctly 1`] = `
</th>
<td
class="ant-descriptions-item-content"
colspan="5"
colspan="1"
>
<span>
Data disk type: MongoDB
@ -1012,13 +1012,28 @@ exports[`renders components/descriptions/demo/responsive.tsx correctly 1`] = `
Database version: 3.4
<br />
Package: dds.mongo.mid
</span>
</td>
<th
class="ant-descriptions-item-label"
colspan="1"
>
<span>
Hardware Info
</span>
</th>
<td
class="ant-descriptions-item-content"
colspan="3"
>
<span>
CPU: 6 Core 3.5 GHz
<br />
Storage space: 10 GB
<br />
Replication factor: 3
<br />
Region: East China 1
<br />
</span>
</td>
</tr>

View File

@ -1,9 +1,10 @@
import MockDate from 'mockdate';
import React from 'react';
import MockDate from 'mockdate';
import Descriptions from '..';
import { resetWarned } from '../../_util/warning';
import mountTest from '../../../tests/shared/mountTest';
import { render } from '../../../tests/utils';
import { resetWarned } from '../../_util/warning';
import ConfigProvider from '../../config-provider';
describe('Descriptions', () => {
@ -20,7 +21,7 @@ describe('Descriptions', () => {
errorSpy.mockRestore();
});
it('when max-width: 575pxcolumn=1', () => {
it('when max-width: 575px, column=1', () => {
const wrapper = render(
<Descriptions>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
@ -35,7 +36,7 @@ describe('Descriptions', () => {
wrapper.unmount();
});
it('when max-width: 575pxcolumn=2', () => {
it('when max-width: 575px, column=2', () => {
// eslint-disable-next-line global-require
const wrapper = render(
<Descriptions column={{ xs: 2 }}>
@ -49,6 +50,36 @@ describe('Descriptions', () => {
wrapper.unmount();
});
it('when max-width: 575px, column=2, span=2', () => {
// eslint-disable-next-line global-require
const { container } = render(
<Descriptions
column={{ xs: 2 }}
items={[
{
label: 'Product',
children: 'Cloud Database',
span: { xs: 2 },
},
{
label: 'Billing',
children: 'Prepaid',
span: { xs: 1 },
},
{
label: 'Time',
children: '18:00:00',
span: { xs: 1 },
},
]}
/>,
);
expect(container.querySelectorAll('.ant-descriptions-item')[0]).toHaveAttribute('colSpan', '2');
expect(container.querySelectorAll('.ant-descriptions-item')[1]).toHaveAttribute('colSpan', '1');
expect(container.querySelectorAll('.ant-descriptions-item')[2]).toHaveAttribute('colSpan', '1');
});
it('column is number', () => {
// eslint-disable-next-line global-require
const wrapper = render(

View File

@ -0,0 +1,12 @@
import type { Breakpoint } from '../_util/responsiveObserver';
const DEFAULT_COLUMN_MAP: Record<Breakpoint, number> = {
xxl: 3,
xl: 3,
lg: 3,
md: 3,
sm: 2,
xs: 1,
};
export default DEFAULT_COLUMN_MAP;

View File

@ -4,38 +4,34 @@ import type { DescriptionsProps } from 'antd';
const items: DescriptionsProps['items'] = [
{
key: '1',
label: 'Product',
children: 'Cloud Database',
},
{
key: '2',
label: 'Billing',
children: 'Prepaid',
},
{
key: '3',
label: 'Time',
children: '18:00:00',
},
{
key: '4',
label: 'Amount',
children: '$80.00',
},
{
key: '5',
label: 'Discount',
span: { xl: 2, xxl: 2 },
children: '$20.00',
},
{
key: '6',
label: 'Official',
span: { xl: 2, xxl: 2 },
children: '$60.00',
},
{
key: '7',
label: 'Config Info',
span: { xs: 1, sm: 2, md: 3, lg: 3, xl: 2, xxl: 2 },
children: (
<>
Data disk type: MongoDB
@ -43,13 +39,21 @@ const items: DescriptionsProps['items'] = [
Database version: 3.4
<br />
Package: dds.mongo.mid
</>
),
},
{
label: 'Hardware Info',
span: { xs: 1, sm: 2, md: 3, lg: 3, xl: 2, xxl: 2 },
children: (
<>
CPU: 6 Core 3.5 GHz
<br />
Storage space: 10 GB
<br />
Replication factor: 3
<br />
Region: East China 1
<br />
</>
),
},
@ -59,7 +63,7 @@ const App: React.FC = () => (
<Descriptions
title="Responsive Descriptions"
bordered
column={{ xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 }}
column={{ xs: 1, sm: 2, md: 3, lg: 3, xl: 4, xxl: 4 }}
items={items}
/>
);

View File

@ -0,0 +1,33 @@
import * as React from 'react';
import toArray from 'rc-util/lib/Children/toArray';
import type { DescriptionsItemType, InternalDescriptionsItemType } from '..';
import { matchScreen, type ScreenMap } from '../../_util/responsiveObserver';
// Convert children into items
const transChildren2Items = (childNodes?: React.ReactNode) =>
toArray(childNodes).map((node) => ({ ...node?.props }));
export default function useItems(
screens: ScreenMap,
items?: DescriptionsItemType[],
children?: React.ReactNode,
) {
const mergedItems = React.useMemo<DescriptionsItemType[]>(
() =>
// Take `items` first or convert `children` into items
items || transChildren2Items(children),
[items, children],
);
const responsiveItems = React.useMemo<InternalDescriptionsItemType[]>(
() =>
mergedItems.map(({ span, ...restItem }) => ({
...restItem,
span: typeof span === 'number' ? span : matchScreen(screens, span),
})),
[mergedItems, screens],
);
return responsiveItems;
}

View File

@ -1,14 +1,13 @@
import toArray from 'rc-util/lib/Children/toArray';
import type React from 'react';
import { useMemo } from 'react';
import type { DescriptionsItemType } from '..';
import type { InternalDescriptionsItemType } from '..';
import warning from '../../_util/warning';
function getFilledItem(
rowItem: DescriptionsItemType,
rowItem: InternalDescriptionsItemType,
rowRestCol: number,
span?: number,
): DescriptionsItemType {
): InternalDescriptionsItemType {
let clone = rowItem;
if (span === undefined || span > rowRestCol) {
@ -25,14 +24,10 @@ function getFilledItem(
return clone;
}
// Convert children into items
const transChildren2Items = (childNodes?: React.ReactNode) =>
toArray(childNodes).map((node) => ({ ...node?.props }));
// Calculate the sum of span in a row
function getCalcRows(rowItems: DescriptionsItemType[], mergedColumn: number) {
const rows: DescriptionsItemType[][] = [];
let tmpRow: DescriptionsItemType[] = [];
function getCalcRows(rowItems: InternalDescriptionsItemType[], mergedColumn: number) {
const rows: InternalDescriptionsItemType[][] = [];
let tmpRow: InternalDescriptionsItemType[] = [];
let rowRestCol = mergedColumn;
rowItems
@ -62,17 +57,8 @@ function getCalcRows(rowItems: DescriptionsItemType[], mergedColumn: number) {
return rows;
}
const useRow = (
mergedColumn: number,
items?: DescriptionsItemType[],
children?: React.ReactNode,
) => {
const rows = useMemo(() => {
if (Array.isArray(items)) {
return getCalcRows(items, mergedColumn);
}
return getCalcRows(transChildren2Items(children), mergedColumn);
}, [items, children, mergedColumn]);
const useRow = (mergedColumn: number, items: InternalDescriptionsItemType[]) => {
const rows = useMemo(() => getCalcRows(items, mergedColumn), [items, mergedColumn]);
return rows;
};

View File

@ -93,12 +93,12 @@ Common props ref[Common props](/docs/react/common-props)
### DescriptionItem
| Property | Description | Type | Default | Version |
| ------------ | ------------------------------ | ------------- | ------- | ------- |
| contentStyle | Customize content style | CSSProperties | - | 4.9.0 |
| label | The description of the content | ReactNode | - | |
| labelStyle | Customize label style | CSSProperties | - | 4.9.0 |
| span | The number of columns included | number | 1 | |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| contentStyle | Customize content style | CSSProperties | - | 4.9.0 |
| label | The description of the content | ReactNode | - | |
| labelStyle | Customize label style | CSSProperties | - | 4.9.0 |
| span | The number of columns included | number \| [Screens](/components/grid#col) | 1 | `screens: 5.9.0` |
> The number of span Description.Item. Span={2} takes up the width of two DescriptionItems. When both `style` and `labelStyle`(or `contentStyle`) configured, both of them will work. And next one will overwrite first when conflict.

View File

@ -1,53 +1,35 @@
'use client';
/* eslint-disable react/no-array-index-key */
import classNames from 'classnames';
import * as React from 'react';
import type { Breakpoint, ScreenMap } from '../_util/responsiveObserver';
import useResponsiveObserver, { responsiveArray } from '../_util/responsiveObserver';
import classNames from 'classnames';
import type { Breakpoint } from '../_util/responsiveObserver';
import { matchScreen } from '../_util/responsiveObserver';
import { ConfigContext } from '../config-provider';
import useSize from '../config-provider/hooks/useSize';
import useBreakpoint from '../grid/hooks/useBreakpoint';
import DEFAULT_COLUMN_MAP from './constant';
import DescriptionsContext from './DescriptionsContext';
import useItems from './hooks/useItems';
import useRow from './hooks/useRow';
import type { DescriptionsItemProps } from './Item';
import DescriptionsItem from './Item';
import Row from './Row';
import useRow from './hooks/useRow';
import useStyle from './style';
const DEFAULT_COLUMN_MAP: Record<Breakpoint, number> = {
xxl: 3,
xl: 3,
lg: 3,
md: 3,
sm: 2,
xs: 1,
};
function getColumn(column: DescriptionsProps['column'], screens: ScreenMap): number {
if (typeof column === 'number') {
return column;
}
if (typeof column === 'object') {
for (let i = 0; i < responsiveArray.length; i++) {
const breakpoint: Breakpoint = responsiveArray[i];
if (screens[breakpoint] && column[breakpoint] !== undefined) {
return column[breakpoint] || DEFAULT_COLUMN_MAP[breakpoint];
}
}
}
return 3;
}
interface CompoundedComponent {
Item: typeof DescriptionsItem;
}
export interface DescriptionsItemType extends DescriptionsItemProps {
export interface InternalDescriptionsItemType extends DescriptionsItemProps {
key?: React.Key;
}
export interface DescriptionsItemType extends Omit<InternalDescriptionsItemType, 'span'> {
span?: number | { [key in Breakpoint]?: number };
}
export interface DescriptionsProps {
prefixCls?: string;
className?: string;
@ -74,7 +56,7 @@ const Descriptions: React.FC<DescriptionsProps> & CompoundedComponent = (props)
prefixCls: customizePrefixCls,
title,
extra,
column = DEFAULT_COLUMN_MAP,
column,
colon = true,
bordered,
layout,
@ -90,28 +72,29 @@ const Descriptions: React.FC<DescriptionsProps> & CompoundedComponent = (props)
} = props;
const { getPrefixCls, direction, descriptions } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('descriptions', customizePrefixCls);
const [screens, setScreens] = React.useState<ScreenMap>({});
const mergedColumn = getColumn(column, screens);
const screens = useBreakpoint();
// Column count
const mergedColumn = React.useMemo(() => {
if (typeof column === 'number') {
return column;
}
return (
matchScreen(screens, {
...DEFAULT_COLUMN_MAP,
...column,
}) ?? 3
);
}, [screens, column]);
// Items with responsive
const mergedItems = useItems(screens, items, children);
const mergedSize = useSize(customizeSize);
const rows = useRow(mergedColumn, items, children);
const rows = useRow(mergedColumn, mergedItems);
const [wrapSSR, hashId] = useStyle(prefixCls);
const responsiveObserver = useResponsiveObserver();
// Responsive
React.useEffect(() => {
const token = responsiveObserver.subscribe((newScreens) => {
if (typeof column !== 'object') {
return;
}
setScreens(newScreens);
});
return () => {
responsiveObserver.unsubscribe(token);
};
}, []);
// ======================== Render ========================
const contextValue = React.useMemo(

View File

@ -94,12 +94,12 @@ const items: DescriptionsProps['items'] = [
### DescriptionItem
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| ------------ | -------------- | ------------- | ------ | ----- |
| contentStyle | 自定义内容样式 | CSSProperties | - | 4.9.0 |
| label | 内容的描述 | ReactNode | - | |
| labelStyle | 自定义标签样式 | CSSProperties | - | 4.9.0 |
| span | 包含列的数量 | number | 1 | |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| contentStyle | 自定义内容样式 | CSSProperties | - | 4.9.0 |
| label | 内容的描述 | ReactNode | - | |
| labelStyle | 自定义标签样式 | CSSProperties | - | 4.9.0 |
| span | 包含列的数量 | number \| [Screens](/components/grid#col) | 1 | `screens: 5.9.0` |
> span 是 Description.Item 的数量。 span={2} 会占用两个 DescriptionItem 的宽度。当同时配置 `style``labelStyle`(或 `contentStyle`)时,两者会同时作用。样式冲突时,后者会覆盖前者。