mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-27 20:49:53 +08:00
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:
parent
b0dd138fba
commit
cbfb126690
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
<Descriptions>
|
||||
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
|
||||
@ -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(
|
||||
<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(
|
||||
|
12
components/descriptions/constant.ts
Normal file
12
components/descriptions/constant.ts
Normal 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;
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
33
components/descriptions/hooks/useItems.ts
Normal file
33
components/descriptions/hooks/useItems.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
};
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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`)时,两者会同时作用。样式冲突时,后者会覆盖前者。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user