feat: Modal width support responsive size (#51653)
Some checks failed
Publish Any Commit / build (push) Has been cancelled
🔀 Sync mirror to Gitee / mirror (push) Has been cancelled
✅ test / lint (push) Has been cancelled
✅ test / test-react-legacy (16, 1/2) (push) Has been cancelled
✅ test / test-react-legacy (16, 2/2) (push) Has been cancelled
✅ test / test-react-legacy (17, 1/2) (push) Has been cancelled
✅ test / test-react-legacy (17, 2/2) (push) Has been cancelled
✅ test / test-node (push) Has been cancelled
✅ test / test-react-latest (dom, 1/2) (push) Has been cancelled
✅ test / test-react-latest (dom, 2/2) (push) Has been cancelled
✅ test / build (push) Has been cancelled
✅ test / test lib/es module (es, 1/2) (push) Has been cancelled
✅ test / test lib/es module (es, 2/2) (push) Has been cancelled
✅ test / test lib/es module (lib, 1/2) (push) Has been cancelled
✅ test / test lib/es module (lib, 2/2) (push) Has been cancelled
👁️ Visual Regression Persist Start / test image (push) Has been cancelled
✅ test / test-react-latest-dist (dist, 1/2) (push) Has been cancelled
✅ test / test-react-latest-dist (dist, 2/2) (push) Has been cancelled
✅ test / test-react-latest-dist (dist-min, 1/2) (push) Has been cancelled
✅ test / test-react-latest-dist (dist-min, 2/2) (push) Has been cancelled
✅ test / test-coverage (push) Has been cancelled

* feat: Modal support responsive width

* docs: update doc

* test: update snapshot
This commit is contained in:
二货爱吃白萝卜 2024-11-18 10:27:59 +08:00 committed by GitHub
parent 1b8e956a62
commit fdb07d1864
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 163 additions and 40 deletions

View File

@ -1,6 +1,7 @@
import * as React from 'react';
import classNames from 'classnames';
import type { Breakpoint } from '../_util/responsiveObserver';
import type { LiteralUnion } from '../_util/type';
import { ConfigContext } from '../config-provider';
import RowContext from './RowContext';
@ -20,19 +21,15 @@ export interface ColSize {
pull?: ColSpanType;
}
export interface ColProps extends React.HTMLAttributes<HTMLDivElement> {
export interface ColProps
extends React.HTMLAttributes<HTMLDivElement>,
Partial<Record<Breakpoint, ColSpanType | ColSize>> {
flex?: FlexType;
span?: ColSpanType;
order?: ColSpanType;
offset?: ColSpanType;
push?: ColSpanType;
pull?: ColSpanType;
xs?: ColSpanType | ColSize;
sm?: ColSpanType | ColSize;
md?: ColSpanType | ColSize;
lg?: ColSpanType | ColSize;
xl?: ColSpanType | ColSize;
xxl?: ColSpanType | ColSize;
prefixCls?: string;
}

View File

@ -1,7 +1,7 @@
import { unit } from '@ant-design/cssinjs';
import type { CSSObject } from '@ant-design/cssinjs';
import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal';
import type { AliasToken, FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal';
import { genStyleHooks, mergeToken } from '../../theme/internal';
// biome-ignore lint/suspicious/noEmptyInterface: ComponentToken need to be empty by default
@ -183,26 +183,34 @@ export const prepareColComponentToken: GetDefaultToken<'Grid'> = () => ({});
// ============================== Export ==============================
export const useRowStyle = genStyleHooks('Grid', genGridRowStyle, prepareRowComponentToken);
export const getMediaSize = (token: AliasToken) => {
const mediaSizesMap = {
xs: token.screenXSMin,
sm: token.screenSMMin,
md: token.screenMDMin,
lg: token.screenLGMin,
xl: token.screenXLMin,
xxl: token.screenXXLMin,
} as const;
return mediaSizesMap;
};
export const useColStyle = genStyleHooks(
'Grid',
(token) => {
const gridToken: GridColToken = mergeToken<GridColToken>(token, {
gridColumns: 24, // Row is divided into 24 parts in Grid
});
const gridMediaSizesMap = {
'-sm': gridToken.screenSMMin,
'-md': gridToken.screenMDMin,
'-lg': gridToken.screenLGMin,
'-xl': gridToken.screenXLMin,
'-xxl': gridToken.screenXXLMin,
} as const;
type GridMediaSize = keyof typeof gridMediaSizesMap;
const gridMediaSizesMap: Record<string, number> = getMediaSize(gridToken);
delete gridMediaSizesMap.xs;
return [
genGridColStyle(gridToken),
genGridStyle(gridToken, ''),
genGridStyle(gridToken, '-xs'),
Object.keys(gridMediaSizesMap)
.map((key) => genGridMediaStyle(gridToken, gridMediaSizesMap[key as GridMediaSize], key))
.map((key) => genGridMediaStyle(gridToken, gridMediaSizesMap[key], `-${key}`))
.reduce((pre, cur) => ({ ...pre, ...cur }), {}),
];
},

View File

@ -7,6 +7,7 @@ import ContextIsolator from '../_util/ContextIsolator';
import useClosable, { pickClosable } from '../_util/hooks/useClosable';
import { useZIndex } from '../_util/hooks/useZIndex';
import { getTransitionName } from '../_util/motion';
import { Breakpoint } from '../_util/responsiveObserver';
import { canUseDocElement } from '../_util/styleChecker';
import { devUseWarning } from '../_util/warning';
import zIndexContext from '../_util/zindexContext';
@ -124,12 +125,36 @@ const Modal: React.FC<ModalProps> = (props) => {
// ============================ zIndex ============================
const [zIndex, contextZIndex] = useZIndex('Modal', restProps.zIndex);
// =========================== Width ============================
const [numWidth, responsiveWidth] = React.useMemo<
[string | number | undefined, Partial<Record<Breakpoint, string | number>> | undefined]
>(() => {
if (width && typeof width === 'object') {
return [undefined, width];
}
return [width, undefined];
}, [width]);
const responsiveWidthVars = React.useMemo(() => {
const vars: Record<string, string> = {};
if (responsiveWidth) {
Object.keys(responsiveWidth).forEach((breakpoint) => {
const breakpointWidth = responsiveWidth[breakpoint as Breakpoint];
if (breakpointWidth !== undefined) {
vars[`--${prefixCls}-${breakpoint}-width`] =
typeof breakpointWidth === 'number' ? `${breakpointWidth}px` : breakpointWidth;
}
});
}
return vars;
}, [responsiveWidth]);
// =========================== Render ===========================
return wrapCSSVar(
<ContextIsolator form space>
<zIndexContext.Provider value={contextZIndex}>
<Dialog
width={width}
width={numWidth}
{...restProps}
zIndex={zIndex}
getContainer={getContainer === undefined ? getContextPopupContainer : getContainer}
@ -149,7 +174,7 @@ const Modal: React.FC<ModalProps> = (props) => {
transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)}
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)}
className={classNames(hashId, className, modalContext?.className)}
style={{ ...modalContext?.style, ...style }}
style={{ ...modalContext?.style, ...style, ...responsiveWidthVars }}
classNames={{
...modalContext?.classNames,
...modalClassNames,

View File

@ -192,4 +192,20 @@ describe('Modal', () => {
expect(document.querySelector('.first-origin')).toMatchSnapshot();
expect(document.querySelector('.second-props-origin')).toMatchSnapshot();
});
it('responsive width', () => {
render(
<Modal open width={{ xs: '90%', sm: '80%', md: '70%', lg: '60%', xl: '50%', xxl: '40%' }} />,
);
const modalEle = document.querySelector<HTMLDivElement>('.ant-modal')!;
expect(modalEle).toHaveStyle({
'--ant-modal-xs-width': '90%',
'--ant-modal-sm-width': '80%',
'--ant-modal-md-width': '70%',
'--ant-modal-lg-width': '60%',
'--ant-modal-xl-width': '50%',
'--ant-modal-xxl-width': '40%',
});
});
});

View File

@ -1228,14 +1228,26 @@ exports[`renders components/modal/demo/static-info.tsx extend context correctly
exports[`renders components/modal/demo/static-info.tsx extend context correctly 2`] = `[]`;
exports[`renders components/modal/demo/width.tsx extend context correctly 1`] = `
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
type="button"
<div
class="ant-flex ant-flex-align-flex-start ant-flex-gap-middle ant-flex-vertical"
>
<span>
Open Modal of 1000px width
</span>
</button>
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
type="button"
>
<span>
Open Modal of 1000px width
</span>
</button>
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
type="button"
>
<span>
Open Modal of responsive width
</span>
</button>
</div>
`;
exports[`renders components/modal/demo/width.tsx extend context correctly 2`] = `[]`;

View File

@ -1188,14 +1188,26 @@ exports[`renders components/modal/demo/static-info.tsx correctly 1`] = `
`;
exports[`renders components/modal/demo/width.tsx correctly 1`] = `
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
type="button"
<div
class="ant-flex ant-flex-align-flex-start ant-flex-gap-middle ant-flex-vertical"
>
<span>
Open Modal of 1000px width
</span>
</button>
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
type="button"
>
<span>
Open Modal of 1000px width
</span>
</button>
<button
class="ant-btn ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"
type="button"
>
<span>
Open Modal of responsive width
</span>
</button>
</div>
`;
exports[`renders components/modal/demo/wireframe.tsx correctly 1`] = `

View File

@ -1,11 +1,13 @@
import React, { useState } from 'react';
import { Button, Modal } from 'antd';
import { Button, Flex, Modal } from 'antd';
const App: React.FC = () => {
const [open, setOpen] = useState(false);
const [openResponsive, setOpenResponsive] = useState(false);
return (
<>
<Flex vertical gap="middle" align="flex-start">
{/* Basic */}
<Button type="primary" onClick={() => setOpen(true)}>
Open Modal of 1000px width
</Button>
@ -21,7 +23,31 @@ const App: React.FC = () => {
<p>some contents...</p>
<p>some contents...</p>
</Modal>
</>
{/* Responsive */}
<Button type="primary" onClick={() => setOpenResponsive(true)}>
Open Modal of responsive width
</Button>
<Modal
title="Modal responsive width"
centered
open={openResponsive}
onOk={() => setOpenResponsive(false)}
onCancel={() => setOpenResponsive(false)}
width={{
xs: '90%',
sm: '80%',
md: '70%',
lg: '60%',
xl: '50%',
xxl: '40%',
}}
>
<p>some contents...</p>
<p>some contents...</p>
<p>some contents...</p>
</Modal>
</Flex>
);
};

View File

@ -72,7 +72,7 @@ Common props ref[Common props](/docs/react/common-props)
| loading | Show the skeleton | boolean | | 5.18.0 |
| title | The modal dialog's title | ReactNode | - | |
| open | Whether the modal dialog is visible or not | boolean | false | |
| width | Width of the modal dialog | string \| number | 520 | |
| width | Width of the modal dialog | string \| number \| [Breakpoint](/components/grid-cn#col) | 520 | Breakpoint: 5.23.0 |
| wrapClassName | The class name of the container of the modal dialog | string | - | |
| zIndex | The `z-index` of the Modal | number | 1000 | |
| onCancel | Specify a function that will be called when a user clicks mask, close button on top right or Cancel button | function(e) | - | |

View File

@ -73,7 +73,7 @@ demo:
| loading | 显示骨架屏 | boolean | | 5.18.0 |
| title | 标题 | ReactNode | - | |
| open | 对话框是否可见 | boolean | - | |
| width | 宽度 | string \| number | 520 | |
| width | 宽度 | string \| number \| [Breakpoint](/components/grid-cn#col) | 520 | Breakpoint: 5.23.0 |
| wrapClassName | 对话框外层容器的类名 | string | - | |
| zIndex | 设置 Modal 的 `z-index` | number | 1000 | |
| onCancel | 点击遮罩层或右上角叉或取消按钮的回调 | function(e) | - | |

View File

@ -1,10 +1,11 @@
import type React from 'react';
import type { DialogProps } from 'rc-dialog';
import { Breakpoint } from '../_util/responsiveObserver';
import type { ButtonProps, LegacyButtonType } from '../button/button';
import type { DirectionType } from '../config-provider';
interface ModalCommonProps extends Omit<DialogProps, 'footer'> {
interface ModalCommonProps extends Omit<DialogProps, 'footer' | 'width'> {
footer?:
| React.ReactNode
| ((
@ -30,7 +31,7 @@ export interface ModalProps extends ModalCommonProps {
/** Centered Modal */
centered?: boolean;
/** Width of the modal dialog */
width?: string | number;
width?: string | number | Partial<Record<Breakpoint, string | number>>;
/** Text of the OK button */
okText?: React.ReactNode;
/** Button `type` of the OK button */

View File

@ -1,6 +1,7 @@
import type React from 'react';
import { unit } from '@ant-design/cssinjs';
import { getMediaSize } from '../../grid/style';
import { genFocusStyle, resetComponent } from '../../style';
import { initFadeMotion, initZoomMotion } from '../../style/motion';
import type {
@ -386,6 +387,30 @@ const genRTLStyle: GenerateStyle<ModalToken> = (token) => {
};
};
const genResponsiveWidthStyle: GenerateStyle<ModalToken> = (token) => {
const { componentCls } = token;
const gridMediaSizesMap: Record<string, number> = getMediaSize(token);
delete gridMediaSizesMap.xs;
const responsiveStyles = Object.keys(gridMediaSizesMap).map((key) => ({
[`@media (min-width: ${unit(gridMediaSizesMap[key])})`]: {
width: `var(--${componentCls.replace('.', '')}-${key}-width)`,
},
}));
return {
[`${componentCls}-root`]: {
[componentCls]: [
{
width: `var(--${componentCls.replace('.', '')}-xs-width)`,
},
...responsiveStyles,
],
},
};
};
// ============================== Export ==============================
export const prepareToken: (token: Parameters<GenStyleFn<'Modal'>>[0]) => ModalToken = (token) => {
const headerPaddingVertical = token.padding;
@ -453,6 +478,7 @@ export default genStyleHooks(
genRTLStyle(modalToken),
genModalMaskStyle(modalToken),
initZoomMotion(modalToken, 'zoom'),
genResponsiveWidthStyle(modalToken),
];
},
prepareComponentToken,