From 54fb6bd83149afbfded5a1b908c0b3c3f2eab281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B6=E6=9E=AB?= <7971419+crazyair@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:32:20 +0800 Subject: [PATCH] Form.Item support noLabel (#51524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Form.Item support noLabel * feat: doc * feat: test * feat: test * feat: test * feat: review * feat: review * feat: 仅支持 span * feat: review * feat: review * feat: review * feat: review * feat: review * feat: test * feat: test * feat: test * feat: test * feat: test * feat: test * feat: add test * feat: demo * feat: test * feat: test * feat: test * feat: 代码优化 * feat: add labelCol * feat: 代码优化 * feat: 代码优化 * feat: reset * feat: test * feat: test * feat: review * feat: doc --- components/form/FormItemInput.tsx | 35 ++++++- .../__snapshots__/demo-extend.test.ts.snap | 2 +- .../__snapshots__/demo.test.tsx.snap | 2 +- components/form/__tests__/index.test.tsx | 98 +++++++++++++++++++ components/form/demo/basic.tsx | 8 +- components/form/demo/complex-form-control.tsx | 2 +- .../form/demo/getValueProps-normalize.tsx | 2 +- components/form/demo/nest-messages.tsx | 2 +- .../form/demo/time-related-controls.tsx | 2 +- components/form/index.en-US.md | 4 +- components/form/index.zh-CN.md | 2 +- components/grid/__tests__/index.test.tsx | 4 +- 12 files changed, 142 insertions(+), 21 deletions(-) diff --git a/components/form/FormItemInput.tsx b/components/form/FormItemInput.tsx index 6719391a70..9824858013 100644 --- a/components/form/FormItemInput.tsx +++ b/components/form/FormItemInput.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import classNames from 'classnames'; +import { get, set } from 'rc-util'; import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; import type { ColProps } from '../grid/col'; @@ -31,17 +32,21 @@ interface FormItemInputMiscProps { } export interface FormItemInputProps { + labelCol?: ColProps; wrapperCol?: ColProps; extra?: React.ReactNode; status?: ValidateStatus; help?: React.ReactNode; fieldId?: string; + label?: React.ReactNode; } +const GRID_MAX = 24; const FormItemInput: React.FC = (props) => { const { prefixCls, status, + labelCol, wrapperCol, children, errors, @@ -52,19 +57,41 @@ const FormItemInput: React.FC = (pr fieldId, marginBottom, onErrorVisibleChanged, + label, } = props; const baseClassName = `${prefixCls}-item`; const formContext = React.useContext(FormContext); - const mergedWrapperCol: ColProps = wrapperCol || formContext.wrapperCol || {}; + const mergedWrapperCol = React.useMemo(() => { + let mergedWrapper: ColProps = { ...(wrapperCol || formContext.wrapperCol || {}) }; + if (label === null && !labelCol && !wrapperCol && formContext.labelCol) { + const list = [undefined, 'xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const; + + list.forEach((size) => { + const _size = size ? [size] : []; + + const formLabel = get(formContext.labelCol, _size); + const formLabelObj = typeof formLabel === 'object' ? formLabel : {}; + + const wrapper = get(mergedWrapper, _size); + const wrapperObj = typeof wrapper === 'object' ? wrapper : {}; + + if ('span' in formLabelObj && !('offset' in wrapperObj) && formLabelObj.span < GRID_MAX) { + mergedWrapper = set(mergedWrapper, [..._size, 'offset'], formLabelObj.span); + } + }); + } + return mergedWrapper; + }, [wrapperCol, formContext]); const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className); // Pass to sub FormItem should not with col info - const subFormContext = React.useMemo(() => ({ ...formContext }), [formContext]); - delete subFormContext.labelCol; - delete subFormContext.wrapperCol; + const subFormContext = React.useMemo(() => { + const { labelCol, wrapperCol, ...rest } = formContext; + return rest; + }, [formContext]); const extraRef = React.useRef(null); const [extraHeight, setExtraHeight] = React.useState(0); diff --git a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap index 742beb3a97..0e32f7d2f5 100644 --- a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -21143,7 +21143,7 @@ exports[`renders components/form/demo/time-related-controls.tsx extend context c class="ant-row ant-form-item-row" >
{ expect(container.firstChild).toMatchSnapshot(); }); + it('form.item should support label = null', () => { + // base size + const App: React.FC = () => ( +
+ + + + + + +
+ ); + const { container } = render(); + + const items = container.querySelectorAll('.ant-form-item'); + const oneItems = items[0].querySelector('.ant-row')?.querySelectorAll('.ant-col'); + expect(oneItems?.[0]).toHaveClass('ant-col-4'); + expect(oneItems?.[0].className.includes('offset')).toBeFalsy(); + expect(oneItems?.[1]).toHaveClass('ant-col-14'); + expect(oneItems?.[1].className.includes('offset')).toBeFalsy(); + const twoItem = items[1].querySelector('.ant-row')?.querySelector('.ant-col'); + expect(twoItem).toHaveClass('ant-col-14 ant-col-offset-4'); + + // more size + const list = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const; + list.forEach((size) => { + const { container } = render( +
+ + + + + + +
, + ); + + const items = container.querySelectorAll('.ant-form-item'); + const oneItems = items[0].querySelector('.ant-row')?.querySelectorAll('.ant-col'); + expect(oneItems?.[0]).toHaveClass(`ant-col-${size}-4`); + expect(oneItems?.[0].className.includes('offset')).toBeFalsy(); + expect(oneItems?.[1]).toHaveClass('ant-col-14'); + expect(oneItems?.[1].className.includes('offset')).toBeFalsy(); + const twoItem = items[1].querySelector('.ant-row')?.querySelector('.ant-col'); + expect(twoItem).toHaveClass(`ant-col-14 ant-col-${size}-offset-4`); + }); + }); + + it('form.item should support label = null and labelCol.span = 24', () => { + // base size + const App: React.FC = () => ( +
+ + + + + + +
+ ); + const { container } = render(); + + const items = container.querySelectorAll('.ant-form-item'); + const oneItems = items[0].querySelector('.ant-row')?.querySelectorAll('.ant-col'); + expect(oneItems?.[0]).toHaveClass('ant-col-24'); + expect(oneItems?.[0].className.includes('offset')).toBeFalsy(); + expect(oneItems?.[1]).toHaveClass('ant-col-24'); + expect(oneItems?.[1].className.includes('offset')).toBeFalsy(); + const twoItem = items[1].querySelector('.ant-row')?.querySelector('.ant-col'); + expect(twoItem).toHaveClass('ant-col-24'); + expect(twoItem?.className.includes('offset')).toBeFalsy(); + + // more size + const list = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const; + list.forEach((size) => { + const { container } = render( +
+ + + + + + +
, + ); + + const items = container.querySelectorAll('.ant-form-item'); + const oneItems = items[0].querySelector('.ant-row')?.querySelectorAll('.ant-col'); + expect(oneItems?.[0]).toHaveClass(`ant-col-${size}-24`); + expect(oneItems?.[0].className.includes('offset')).toBeFalsy(); + expect(oneItems?.[1]).toHaveClass('ant-col-24'); + expect(oneItems?.[1].className.includes('offset')).toBeFalsy(); + const twoItem = items[1].querySelector('.ant-row')?.querySelector('.ant-col'); + expect(twoItem).toHaveClass(`ant-col-24`); + expect(twoItem?.className.includes('offset')).toBeFalsy(); + }); + }); + it('_internalItemRender api test', () => { const { container } = render(
diff --git a/components/form/demo/basic.tsx b/components/form/demo/basic.tsx index 06c7fdf74c..b8c12aa716 100644 --- a/components/form/demo/basic.tsx +++ b/components/form/demo/basic.tsx @@ -43,15 +43,11 @@ const App: React.FC = () => ( - - name="remember" - valuePropName="checked" - wrapperCol={{ offset: 8, span: 16 }} - > + name="remember" valuePropName="checked" label={null}> Remember me - + diff --git a/components/form/demo/complex-form-control.tsx b/components/form/demo/complex-form-control.tsx index 9e6f47556c..a017fb23e1 100644 --- a/components/form/demo/complex-form-control.tsx +++ b/components/form/demo/complex-form-control.tsx @@ -66,7 +66,7 @@ const App: React.FC = () => ( - + diff --git a/components/form/demo/getValueProps-normalize.tsx b/components/form/demo/getValueProps-normalize.tsx index 9fa580f3d1..30023ad20a 100644 --- a/components/form/demo/getValueProps-normalize.tsx +++ b/components/form/demo/getValueProps-normalize.tsx @@ -33,7 +33,7 @@ const App: React.FC = () => ( - + diff --git a/components/form/demo/nest-messages.tsx b/components/form/demo/nest-messages.tsx index 7aa1807499..688c0ccdbe 100644 --- a/components/form/demo/nest-messages.tsx +++ b/components/form/demo/nest-messages.tsx @@ -44,7 +44,7 @@ const App: React.FC = () => ( - + diff --git a/components/form/demo/time-related-controls.tsx b/components/form/demo/time-related-controls.tsx index 736613cc78..011ffe2034 100644 --- a/components/form/demo/time-related-controls.tsx +++ b/components/form/demo/time-related-controls.tsx @@ -66,7 +66,7 @@ const App: React.FC = () => ( - + diff --git a/components/form/index.en-US.md b/components/form/index.en-US.md index 6899392d5c..87ad54997d 100644 --- a/components/form/index.en-US.md +++ b/components/form/index.en-US.md @@ -136,8 +136,8 @@ Form field component for data bidirectional binding, validation, layout, and so | hidden | Whether to hide Form.Item (still collect and validate value) | boolean | false | 4.4.0 | | htmlFor | Set sub label `htmlFor` | string | - | | | initialValue | Config sub default value. Form `initialValues` get higher priority when conflict | string | - | 4.2.0 | -| label | Label text | ReactNode | - | | -| labelAlign | The text align of label | `left` \| `right` | `right` | | +| label | Label text. When there is no need for a label but it needs to be aligned with a colon, it can be set to null | ReactNode | - | null: 5.22.0 | +| labelAlign | The text align of label, | `left` \| `right` | `right` | | | labelCol | The layout of label. You can set `span` `offset` to something like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` same as with ``. You can set `labelCol` on Form which will not affect nest Item. If both exists, use Item first | [object](/components/grid/#col) | - | | | messageVariables | The default validate field info, description [see below](#messagevariables) | Record<string, string> | - | 4.7.0 | | name | Field name, support array | [NamePath](#namepath) | - | | diff --git a/components/form/index.zh-CN.md b/components/form/index.zh-CN.md index 81d5ea50d9..5dceb2d895 100644 --- a/components/form/index.zh-CN.md +++ b/components/form/index.zh-CN.md @@ -137,7 +137,7 @@ const validateMessages = { | hidden | 是否隐藏字段(依然会收集和校验字段) | boolean | false | 4.4.0 | | htmlFor | 设置子元素 label `htmlFor` 属性 | string | - | | | initialValue | 设置子元素默认值,如果与 Form 的 `initialValues` 冲突则以 Form 为准 | string | - | 4.2.0 | -| label | `label` 标签的文本 | ReactNode | - | | +| label | `label` 标签的文本,当不需要 label 又需要与冒号对齐,可以设为 null | ReactNode | - | null: 5.22.0 | | labelAlign | 标签文本对齐方式 | `left` \| `right` | `right` | | | labelCol | `label` 标签布局,同 `` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}` 或 `sm: {span: 3, offset: 12}`。你可以通过 Form 的 `labelCol` 进行统一设置,不会作用于嵌套 Item。当和 Form 同时设置时,以 Item 为准 | [object](/components/grid-cn#col) | - | | | messageVariables | 默认验证字段的信息,查看[详情](#messagevariables) | Record<string, string> | - | 4.7.0 | diff --git a/components/grid/__tests__/index.test.tsx b/components/grid/__tests__/index.test.tsx index b903bd04f2..4ba52863f6 100644 --- a/components/grid/__tests__/index.test.tsx +++ b/components/grid/__tests__/index.test.tsx @@ -214,14 +214,14 @@ describe('Grid', () => { // https://github.com/ant-design/ant-design/issues/39690 it('Justify and align properties should reactive for Row', () => { const ReactiveTest = () => { - const [justify, setjustify] = useState('start'); + const [justify, setJustify] = useState('start'); return ( <>
button1
button
- setjustify('end')} /> + setJustify('end')} /> ); };