Form.Item support noLabel (#51524)

* 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
This commit is contained in:
叶枫 2024-11-08 18:32:20 +08:00 committed by GitHub
parent 868d344d90
commit 54fb6bd831
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 142 additions and 21 deletions

View File

@ -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<FormItemInputProps & FormItemInputMiscProps> = (props) => {
const {
prefixCls,
status,
labelCol,
wrapperCol,
children,
errors,
@ -52,19 +57,41 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = (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<HTMLDivElement>(null);
const [extraHeight, setExtraHeight] = React.useState<number>(0);

View File

@ -21143,7 +21143,7 @@ exports[`renders components/form/demo/time-related-controls.tsx extend context c
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-xs-offset-0 ant-col-sm-16 ant-col-sm-offset-8"
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-16 ant-col-sm-offset-8"
>
<div
class="ant-form-item-control-input"

View File

@ -8747,7 +8747,7 @@ exports[`renders components/form/demo/time-related-controls.tsx correctly 1`] =
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-xs-offset-0 ant-col-sm-16 ant-col-sm-offset-8"
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-16 ant-col-sm-offset-8"
>
<div
class="ant-form-item-control-input"

View File

@ -1367,6 +1367,104 @@ describe('Form', () => {
expect(container.firstChild).toMatchSnapshot();
});
it('form.item should support label = null', () => {
// base size
const App: React.FC = () => (
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 14 }}>
<Form.Item label="name" name="name">
<Input />
</Form.Item>
<Form.Item label={null}>
<Button>Submit</Button>
</Form.Item>
</Form>
);
const { container } = render(<App />);
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(
<Form labelCol={{ [size]: { span: 4 } }} wrapperCol={{ span: 14 }}>
<Form.Item label="name" name="name">
<Input />
</Form.Item>
<Form.Item label={null}>
<Button>Submit</Button>
</Form.Item>
</Form>,
);
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 = () => (
<Form labelCol={{ span: 24 }} wrapperCol={{ span: 24 }}>
<Form.Item label="name" name="name">
<Input />
</Form.Item>
<Form.Item label={null}>
<Button>Submit</Button>
</Form.Item>
</Form>
);
const { container } = render(<App />);
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(
<Form labelCol={{ [size]: { span: 24 } }} wrapperCol={{ span: 24 }}>
<Form.Item label="name" name="name">
<Input />
</Form.Item>
<Form.Item label={null}>
<Button>Submit</Button>
</Form.Item>
</Form>,
);
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(
<Form>

View File

@ -43,15 +43,11 @@ const App: React.FC = () => (
<Input.Password />
</Form.Item>
<Form.Item<FieldType>
name="remember"
valuePropName="checked"
wrapperCol={{ offset: 8, span: 16 }}
>
<Form.Item<FieldType> name="remember" valuePropName="checked" label={null}>
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Form.Item label={null}>
<Button type="primary" htmlType="submit">
Submit
</Button>

View File

@ -66,7 +66,7 @@ const App: React.FC = () => (
<Input placeholder="Input birth month" />
</Form.Item>
</Form.Item>
<Form.Item label=" " colon={false}>
<Form.Item label={null}>
<Button type="primary" htmlType="submit">
Submit
</Button>

View File

@ -33,7 +33,7 @@ const App: React.FC = () => (
<DatePicker />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Form.Item label={null}>
<Button type="primary" htmlType="submit">
Submit
</Button>

View File

@ -44,7 +44,7 @@ const App: React.FC = () => (
<Form.Item name={['user', 'introduction']} label="Introduction">
<Input.TextArea />
</Form.Item>
<Form.Item wrapperCol={{ ...layout.wrapperCol, offset: 8 }}>
<Form.Item label={null}>
<Button type="primary" htmlType="submit">
Submit
</Button>

View File

@ -66,7 +66,7 @@ const App: React.FC = () => (
<Form.Item name="time-picker" label="TimePicker" {...config}>
<TimePicker />
</Form.Item>
<Form.Item wrapperCol={{ xs: { span: 24, offset: 0 }, sm: { span: 16, offset: 8 } }}>
<Form.Item label={null}>
<Button type="primary" htmlType="submit">
Submit
</Button>

View File

@ -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 `<Col>`. 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&lt;string, string> | - | 4.7.0 |
| name | Field name, support array | [NamePath](#namepath) | - | |

View File

@ -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` 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}``sm: {span: 3, offset: 12}`。你可以通过 Form 的 `labelCol` 进行统一设置,不会作用于嵌套 Item。当和 Form 同时设置时,以 Item 为准 | [object](/components/grid-cn#col) | - | |
| messageVariables | 默认验证字段的信息,查看[详情](#messagevariables) | Record&lt;string, string> | - | 4.7.0 |

View File

@ -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<any>('start');
const [justify, setJustify] = useState<any>('start');
return (
<>
<Row justify={justify} align="bottom">
<div>button1</div>
<div>button</div>
</Row>
<span onClick={() => setjustify('end')} />
<span onClick={() => setJustify('end')} />
</>
);
};