From cbfb126690b836460bb3ffb93859f1dde88ffa8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Wed, 30 Aug 2023 22:09:32 +0800 Subject: [PATCH] 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 --- components/_util/responsiveObserver.ts | 12 +++ components/descriptions/Row.tsx | 7 +- .../__snapshots__/demo-extend.test.ts.snap | 21 ++++- .../__tests__/__snapshots__/demo.test.ts.snap | 19 ++++- .../descriptions/__tests__/index.test.tsx | 39 ++++++++- components/descriptions/constant.ts | 12 +++ components/descriptions/demo/responsive.tsx | 22 +++-- components/descriptions/hooks/useItems.ts | 33 ++++++++ components/descriptions/hooks/useRow.ts | 32 ++----- components/descriptions/index.en-US.md | 12 +-- components/descriptions/index.tsx | 83 ++++++++----------- components/descriptions/index.zh-CN.md | 12 +-- 12 files changed, 200 insertions(+), 104 deletions(-) create mode 100644 components/descriptions/constant.ts create mode 100644 components/descriptions/hooks/useItems.ts diff --git a/components/_util/responsiveObserver.ts b/components/_util/responsiveObserver.ts index a8fd54df78..ca4b3f7844 100644 --- a/components/_util/responsiveObserver.ts +++ b/components/_util/responsiveObserver.ts @@ -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]; + } + } + } +}; diff --git a/components/descriptions/Row.tsx b/components/descriptions/Row.tsx index 2242c0a0bb..b2e7d62c10 100644 --- a/components/descriptions/Row.tsx +++ b/components/descriptions/Row.tsx @@ -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; diff --git a/components/descriptions/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/descriptions/__tests__/__snapshots__/demo-extend.test.ts.snap index 4017157a71..aeff573553 100644 --- a/components/descriptions/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/descriptions/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -1116,13 +1116,32 @@ exports[`renders components/descriptions/demo/responsive.tsx extend context corr Database version: 3.4
Package: dds.mongo.mid + + + + + + + Hardware Info + + + + + CPU: 6 Core 3.5 GHz
Storage space: 10 GB
Replication factor: 3
Region: East China 1 -
diff --git a/components/descriptions/__tests__/__snapshots__/demo.test.ts.snap b/components/descriptions/__tests__/__snapshots__/demo.test.ts.snap index c09c543df7..06ca917c6f 100644 --- a/components/descriptions/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/descriptions/__tests__/__snapshots__/demo.test.ts.snap @@ -1004,7 +1004,7 @@ exports[`renders components/descriptions/demo/responsive.tsx correctly 1`] = ` Data disk type: MongoDB @@ -1012,13 +1012,28 @@ exports[`renders components/descriptions/demo/responsive.tsx correctly 1`] = ` Database version: 3.4
Package: dds.mongo.mid +
+ + + + Hardware Info + + + + + CPU: 6 Core 3.5 GHz
Storage space: 10 GB
Replication factor: 3
Region: East China 1 -
diff --git a/components/descriptions/__tests__/index.test.tsx b/components/descriptions/__tests__/index.test.tsx index 54669dbe65..59f182f5d3 100644 --- a/components/descriptions/__tests__/index.test.tsx +++ b/components/descriptions/__tests__/index.test.tsx @@ -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: 575px,column=1', () => { + it('when max-width: 575px, column=1', () => { const wrapper = render( Cloud Database @@ -35,7 +36,7 @@ describe('Descriptions', () => { wrapper.unmount(); }); - it('when max-width: 575px,column=2', () => { + it('when max-width: 575px, column=2', () => { // eslint-disable-next-line global-require const wrapper = render( @@ -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( + , + ); + + 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( diff --git a/components/descriptions/constant.ts b/components/descriptions/constant.ts new file mode 100644 index 0000000000..a42d33092d --- /dev/null +++ b/components/descriptions/constant.ts @@ -0,0 +1,12 @@ +import type { Breakpoint } from '../_util/responsiveObserver'; + +const DEFAULT_COLUMN_MAP: Record = { + xxl: 3, + xl: 3, + lg: 3, + md: 3, + sm: 2, + xs: 1, +}; + +export default DEFAULT_COLUMN_MAP; diff --git a/components/descriptions/demo/responsive.tsx b/components/descriptions/demo/responsive.tsx index 3ff9e3f05a..3116f285b2 100644 --- a/components/descriptions/demo/responsive.tsx +++ b/components/descriptions/demo/responsive.tsx @@ -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
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
Storage space: 10 GB
Replication factor: 3
Region: East China 1 -
), }, @@ -59,7 +63,7 @@ const App: React.FC = () => ( ); diff --git a/components/descriptions/hooks/useItems.ts b/components/descriptions/hooks/useItems.ts new file mode 100644 index 0000000000..357385bde1 --- /dev/null +++ b/components/descriptions/hooks/useItems.ts @@ -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( + () => + // Take `items` first or convert `children` into items + items || transChildren2Items(children), + [items, children], + ); + + const responsiveItems = React.useMemo( + () => + mergedItems.map(({ span, ...restItem }) => ({ + ...restItem, + span: typeof span === 'number' ? span : matchScreen(screens, span), + })), + [mergedItems, screens], + ); + + return responsiveItems; +} diff --git a/components/descriptions/hooks/useRow.ts b/components/descriptions/hooks/useRow.ts index 0b44fe0631..de2c3fcaad 100644 --- a/components/descriptions/hooks/useRow.ts +++ b/components/descriptions/hooks/useRow.ts @@ -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; }; diff --git a/components/descriptions/index.en-US.md b/components/descriptions/index.en-US.md index dc18f189de..91969f1efa 100644 --- a/components/descriptions/index.en-US.md +++ b/components/descriptions/index.en-US.md @@ -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. diff --git a/components/descriptions/index.tsx b/components/descriptions/index.tsx index 6fc7edec2b..6108108c79 100644 --- a/components/descriptions/index.tsx +++ b/components/descriptions/index.tsx @@ -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 = { - 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 { + span?: number | { [key in Breakpoint]?: number }; +} + export interface DescriptionsProps { prefixCls?: string; className?: string; @@ -74,7 +56,7 @@ const Descriptions: React.FC & CompoundedComponent = (props) prefixCls: customizePrefixCls, title, extra, - column = DEFAULT_COLUMN_MAP, + column, colon = true, bordered, layout, @@ -90,28 +72,29 @@ const Descriptions: React.FC & CompoundedComponent = (props) } = props; const { getPrefixCls, direction, descriptions } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('descriptions', customizePrefixCls); - const [screens, setScreens] = React.useState({}); - 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( diff --git a/components/descriptions/index.zh-CN.md b/components/descriptions/index.zh-CN.md index d61a1e9eae..8d32de7ee7 100644 --- a/components/descriptions/index.zh-CN.md +++ b/components/descriptions/index.zh-CN.md @@ -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`)时,两者会同时作用。样式冲突时,后者会覆盖前者。