fix: Form.Item noStyle should not be affected by parent Form.Item (#35849)

* fix: Form.Item noStyle should not be affected by parent Form.Item

* test: update snapshot

* fix: status

* chore: code clean

* fix: modal and drawer

* test: fix lint

* chore: code clean

* refactor: noFormStyle

* chore: code clean

* revert: revert change in Form.Item

* chore: code clean
This commit is contained in:
MadCcc 2022-06-06 23:39:00 +08:00 committed by GitHub
parent 6e04120265
commit 1f080c299e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 244 additions and 116 deletions

View File

@ -3,6 +3,7 @@ import classNames from 'classnames';
import RcDrawer from 'rc-drawer';
import * as React from 'react';
import { ConfigContext } from '../config-provider';
import { NoFormStyle } from '../form/context';
import { tuple } from '../_util/type';
type DrawerRef = {
@ -292,37 +293,39 @@ const Drawer = React.forwardRef<DrawerRef, DrawerProps>(
return (
<DrawerContext.Provider value={operations}>
<RcDrawer
handler={false}
{...{
placement,
prefixCls,
maskClosable,
level,
keyboard,
children,
onClose,
forceRender,
...rest,
}}
{...offsetStyle}
open={visible || propsVisible}
showMask={mask}
style={getRcDrawerStyle()}
className={drawerClassName}
getContainer={getContainer}
afterVisibleChange={open => {
if (open) {
destroyCloseRef.current = false;
} else if (destroyOnClose) {
destroyCloseRef.current = true;
setLoad(false);
}
afterVisibleChange?.(open);
}}
>
{renderBody()}
</RcDrawer>
<NoFormStyle status override>
<RcDrawer
handler={false}
{...{
placement,
prefixCls,
maskClosable,
level,
keyboard,
children,
onClose,
forceRender,
...rest,
}}
{...offsetStyle}
open={visible || propsVisible}
showMask={mask}
style={getRcDrawerStyle()}
className={drawerClassName}
getContainer={getContainer}
afterVisibleChange={open => {
if (open) {
destroyCloseRef.current = false;
} else if (destroyOnClose) {
destroyCloseRef.current = true;
setLoad(false);
}
afterVisibleChange?.(open);
}}
>
{renderBody()}
</RcDrawer>
</NoFormStyle>
</DrawerContext.Provider>
);
},

View File

@ -1,3 +1,3 @@
// deps-lint-skip: empty
// deps-lint-skip: empty, form
import '../../style/index.less';
import './index.less';

View File

@ -1,33 +1,33 @@
import * as React from 'react';
import type { ReactNode } from 'react';
import { useContext, useMemo } from 'react';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import classNames from 'classnames';
import type { FormInstance } from 'rc-field-form';
import { Field, FieldContext, ListContext } from 'rc-field-form';
import type { FieldProps } from 'rc-field-form/lib/Field';
import type { Meta, NamePath } from 'rc-field-form/lib/interface';
import { supportRef } from 'rc-util/lib/ref';
import useState from 'rc-util/lib/hooks/useState';
import omit from 'rc-util/lib/omit';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import Row from '../grid/row';
import { supportRef } from 'rc-util/lib/ref';
import type { ReactNode } from 'react';
import * as React from 'react';
import { useContext, useMemo } from 'react';
import { ConfigContext } from '../config-provider';
import Row from '../grid/row';
import { cloneElement, isValidElement } from '../_util/reactNode';
import { tuple } from '../_util/type';
import warning from '../_util/warning';
import type { FormItemLabelProps, LabelTooltipType } from './FormItemLabel';
import FormItemLabel from './FormItemLabel';
import type { FormItemInputProps } from './FormItemInput';
import FormItemInput from './FormItemInput';
import type { FormItemStatusContextProps } from './context';
import { FormContext, FormItemInputContext, NoStyleItemContext } from './context';
import { toArray, getFieldId } from './util';
import { cloneElement, isValidElement } from '../_util/reactNode';
import useFrameState from './hooks/useFrameState';
import type { FormItemInputProps } from './FormItemInput';
import FormItemInput from './FormItemInput';
import type { FormItemLabelProps, LabelTooltipType } from './FormItemLabel';
import FormItemLabel from './FormItemLabel';
import useDebounce from './hooks/useDebounce';
import useFrameState from './hooks/useFrameState';
import useItemRef from './hooks/useItemRef';
import { getFieldId, toArray } from './util';
const NAME_SPLIT = '__SPLIT__';
@ -217,9 +217,14 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
const getItemRef = useItemRef();
// ======================== Status ========================
let mergedValidateStatus: ValidateStatus = '';
const { status: contextStatus, hasFeedback: contextHasFeedback } =
useContext(FormItemInputContext);
let mergedValidateStatus: ValidateStatus | undefined;
if (validateStatus !== undefined) {
mergedValidateStatus = validateStatus;
} else if (contextStatus !== undefined) {
mergedValidateStatus = contextStatus;
} else if (meta?.validating) {
mergedValidateStatus = 'validating';
} else if (debounceErrors.length) {
@ -230,9 +235,11 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
mergedValidateStatus = 'success';
}
const mergedHasFeedback = hasFeedback || contextHasFeedback;
const formItemStatusContext = useMemo<FormItemStatusContextProps>(() => {
let feedbackIcon: ReactNode;
if (hasFeedback) {
if (mergedHasFeedback) {
const IconNode = mergedValidateStatus && iconMap[mergedValidateStatus];
feedbackIcon = IconNode ? (
<span
@ -248,11 +255,19 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
return {
status: mergedValidateStatus,
hasFeedback,
hasFeedback: mergedHasFeedback,
feedbackIcon,
isFormItemInput: true,
};
}, [mergedValidateStatus, hasFeedback]);
}, [mergedValidateStatus, mergedHasFeedback]);
const noOverrideFormItemContext = useMemo<FormItemStatusContextProps>(
() => ({
...formItemStatusContext,
isFormItemInput: false,
}),
[formItemStatusContext],
);
// ======================== Render ========================
function renderLayout(
@ -261,7 +276,11 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
isRequired?: boolean,
): React.ReactNode {
if (noStyle && !hidden) {
return baseChildren;
return (
<FormItemInputContext.Provider value={noOverrideFormItemContext}>
{baseChildren}
</FormItemInputContext.Provider>
);
}
const itemClassName = {
@ -271,7 +290,7 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
[`${className}`]: !!className,
// Status
[`${prefixCls}-item-has-feedback`]: mergedValidateStatus && hasFeedback,
[`${prefixCls}-item-has-feedback`]: mergedValidateStatus && mergedHasFeedback,
[`${prefixCls}-item-has-success`]: mergedValidateStatus === 'success',
[`${prefixCls}-item-has-warning`]: mergedValidateStatus === 'warning',
[`${prefixCls}-item-has-error`]: mergedValidateStatus === 'error',

View File

@ -6836,7 +6836,7 @@ exports[`renders ./components/form/demo/normal-login.md extend context correctly
class="ant-form-item-control-input-content"
>
<label
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked ant-checkbox-wrapper-in-form-item"
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked"
>
<span
class="ant-checkbox ant-checkbox-checked"
@ -18031,7 +18031,7 @@ exports[`renders ./components/form/demo/validate-other.md extend context correct
class="ant-form-item-control-input-content"
>
<div
class="ant-input-number ant-input-number-in-form-item"
class="ant-input-number"
>
<div
class="ant-input-number-handler-wrap"

View File

@ -4451,7 +4451,7 @@ exports[`renders ./components/form/demo/normal-login.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<label
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked ant-checkbox-wrapper-in-form-item"
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked"
>
<span
class="ant-checkbox ant-checkbox-checked"
@ -7257,7 +7257,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<div
class="ant-input-number ant-input-number-in-form-item"
class="ant-input-number"
>
<div
class="ant-input-number-handler-wrap"

View File

@ -1,27 +1,29 @@
import React, { Component, useState } from 'react';
import { mount } from 'enzyme';
import React, { Component, useState } from 'react';
import { act } from 'react-dom/test-utils';
import scrollIntoView from 'scroll-into-view-if-needed';
import Form from '..';
import * as Util from '../util';
import Input from '../../input';
import Button from '../../button';
import Input from '../../input';
import Select from '../../select';
import Checkbox from '../../checkbox';
import Radio from '../../radio';
import TreeSelect from '../../tree-select';
import Cascader from '../../cascader';
import Checkbox from '../../checkbox';
import DatePicker from '../../date-picker';
import InputNumber from '../../input-number';
import Radio from '../../radio';
import Switch from '../../switch';
import TreeSelect from '../../tree-select';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { sleep, render, fireEvent } from '../../../tests/utils';
import { fireEvent, render, sleep } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import Drawer from '../../drawer';
import zhCN from '../../locale/zh_CN';
import Modal from '../../modal';
const { RangePicker } = DatePicker;
const { TextArea } = Input;
@ -1204,4 +1206,64 @@ describe('Form', () => {
render(<Demo />);
expect(subFormInstance).toBe(formInstance);
});
it('noStyle should not be affected by parent', () => {
const Demo = () => (
<Form>
<Form.Item>
<Form.Item noStyle>
<Select className="custom-select" />
</Form.Item>
</Form.Item>
</Form>
);
const { container } = render(<Demo />);
expect(container.querySelector('.custom-select')?.className).not.toContain('in-form-item');
});
it('noStyle should not affect status', () => {
const Demo = () => (
<Form>
<Form.Item validateStatus="error" noStyle>
<Select className="custom-select" />
</Form.Item>
<Form.Item validateStatus="error">
<Form.Item noStyle>
<Select className="custom-select-b" />
</Form.Item>
</Form.Item>
<Form.Item validateStatus="error">
<Form.Item noStyle validateStatus="warning">
<Select className="custom-select-c" />
</Form.Item>
</Form.Item>
</Form>
);
const { container } = render(<Demo />);
expect(container.querySelector('.custom-select')?.className).toContain('status-error');
expect(container.querySelector('.custom-select-b')?.className).toContain('status-error');
expect(container.querySelector('.custom-select-c')?.className).toContain('status-warning');
});
it('should not affect Popup children style', () => {
const Demo = () => (
<Form>
<Form.Item labelCol={4} validateStatus="error">
<Modal visible>
<Select className="modal-select" />
</Modal>
</Form.Item>
<Form.Item validateStatus="error">
<Drawer visible>
<Select className="drawer-select" />
</Drawer>
</Form.Item>
</Form>
);
const { container } = render(<Demo />, { container: document.body });
expect(container.querySelector('.modal-select')?.className).not.toContain('in-form-item');
expect(container.querySelector('.modal-select')?.className).not.toContain('status-error');
expect(container.querySelector('.drawer-select')?.className).not.toContain('in-form-item');
expect(container.querySelector('.drawer-select')?.className).not.toContain('status-error');
});
});

View File

@ -1,14 +1,14 @@
import * as React from 'react';
import omit from 'rc-util/lib/omit';
import type { Meta } from 'rc-field-form/lib/interface';
import { FormProvider as RcFormProvider } from 'rc-field-form';
import type { FormProviderProps as RcFormProviderProps } from 'rc-field-form/lib/FormContext';
import type { Meta } from 'rc-field-form/lib/interface';
import omit from 'rc-util/lib/omit';
import type { FC, PropsWithChildren, ReactNode } from 'react';
import { useMemo } from 'react';
import * as React from 'react';
import { useContext, useMemo } from 'react';
import type { ColProps } from '../grid/col';
import type { FormLabelAlign } from './interface';
import type { FormInstance, RequiredMark } from './Form';
import type { ValidateStatus } from './FormItem';
import type { FormLabelAlign } from './interface';
/** Form Context. Set top form style and pass to Form Item usage. */
export interface FormContextProps {
@ -63,10 +63,30 @@ export interface FormItemStatusContextProps {
export const FormItemInputContext = React.createContext<FormItemStatusContextProps>({});
export const NoFormStatus: FC<PropsWithChildren<{}>> = ({ children }: PropsWithChildren<{}>) => {
const emptyContext = useMemo(() => ({}), []);
export type NoFormStyleProps = PropsWithChildren<{
status?: boolean;
override?: boolean;
}>;
export const NoFormStyle: FC<NoFormStyleProps> = ({ children, status, override }) => {
const formItemInputContext = useContext(FormItemInputContext);
const newFormItemInputContext = useMemo(() => {
const newContext = { ...formItemInputContext };
if (override) {
delete newContext.isFormItemInput;
}
if (status) {
delete newContext.status;
delete newContext.hasFeedback;
delete newContext.feedbackIcon;
}
return newContext;
}, [status, override, formItemInputContext]);
return (
<FormItemInputContext.Provider value={emptyContext}>{children}</FormItemInputContext.Provider>
<FormItemInputContext.Provider value={newFormItemInputContext}>
{children}
</FormItemInputContext.Provider>
);
};

View File

@ -6,13 +6,13 @@ import RcInputNumber from 'rc-input-number';
import * as React from 'react';
import { useContext } from 'react';
import { ConfigContext } from '../config-provider';
import DisabledContext from '../config-provider/DisabledContext';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import DisabledContext from '../config-provider/DisabledContext';
import { FormItemInputContext, NoFormStatus } from '../form/context';
import { FormItemInputContext, NoFormStyle } from '../form/context';
import { cloneElement } from '../_util/reactNode';
import type { InputStatus } from '../_util/statusUtils';
import { getStatusClassNames, getMergedStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
type ValueType = string | number;
@ -178,9 +178,17 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>((props,
element = (
<div className={mergedGroupClassName} style={props.style}>
<div className={mergedWrapperClassName}>
{addonBeforeNode && <NoFormStatus>{addonBeforeNode}</NoFormStatus>}
{addonBeforeNode && (
<NoFormStyle status override>
{addonBeforeNode}
</NoFormStyle>
)}
{cloneElement(element, { style: null, disabled: mergedDisabled })}
{addonAfterNode && <NoFormStatus>{addonAfterNode}</NoFormStatus>}
{addonAfterNode && (
<NoFormStyle status override>
{addonAfterNode}
</NoFormStyle>
)}
</div>
</div>
);

View File

@ -1,18 +1,18 @@
import React, { forwardRef, useContext, useEffect, useRef } from 'react';
import type { InputProps as RcInputProps, InputRef } from 'rc-input';
import RcInput from 'rc-input';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import classNames from 'classnames';
import type { InputProps as RcInputProps, InputRef } from 'rc-input';
import RcInput from 'rc-input';
import { composeRef } from 'rc-util/lib/ref';
import React, { forwardRef, useContext, useEffect, useRef } from 'react';
import { ConfigContext } from '../config-provider';
import DisabledContext from '../config-provider/DisabledContext';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import DisabledContext from '../config-provider/DisabledContext';
import { FormItemInputContext, NoFormStyle } from '../form/context';
import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import { ConfigContext } from '../config-provider';
import { FormItemInputContext, NoFormStatus } from '../form/context';
import { hasPrefixSuffix } from './utils';
import warning from '../_util/warning';
import { hasPrefixSuffix } from './utils';
export interface InputFocusOptions extends FocusOptions {
cursor?: 'start' | 'end' | 'all';
@ -224,8 +224,20 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
onFocus={handleFocus}
suffix={suffixNode}
allowClear={mergedAllowClear}
addonAfter={addonAfter && <NoFormStatus>{addonAfter}</NoFormStatus>}
addonBefore={addonBefore && <NoFormStatus>{addonBefore}</NoFormStatus>}
addonAfter={
addonAfter && (
<NoFormStyle override status>
{addonAfter}
</NoFormStyle>
)
}
addonBefore={
addonBefore && (
<NoFormStyle override status>
{addonBefore}
</NoFormStyle>
)
}
inputClassName={classNames(
{
[`${prefixCls}-sm`]: mergedSize === 'small',

View File

@ -1,17 +1,18 @@
import * as React from 'react';
import Dialog from 'rc-dialog';
import classNames from 'classnames';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import classNames from 'classnames';
import Dialog from 'rc-dialog';
import * as React from 'react';
import { getConfirmLocale } from './locale';
import Button from '../button';
import type { LegacyButtonType, ButtonProps } from '../button/button';
import type { ButtonProps, LegacyButtonType } from '../button/button';
import { convertLegacyProps } from '../button/button';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import type { DirectionType } from '../config-provider';
import { ConfigContext } from '../config-provider';
import { canUseDocElement } from '../_util/styleChecker';
import { NoFormStyle } from '../form/context';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import { getTransitionName } from '../_util/motion';
import { canUseDocElement } from '../_util/styleChecker';
import { getConfirmLocale } from './locale';
let mousePosition: { x: number; y: number } | null;
@ -201,22 +202,24 @@ const Modal: React.FC<ModalProps> = props => {
[`${prefixCls}-wrap-rtl`]: direction === 'rtl',
});
return (
<Dialog
{...restProps}
getContainer={
getContainer === undefined ? (getContextPopupContainer as getContainerFunc) : getContainer
}
prefixCls={prefixCls}
wrapClassName={wrapClassNameExtended}
footer={footer === undefined ? defaultFooter : footer}
visible={visible}
mousePosition={mousePosition}
onClose={handleCancel}
closeIcon={closeIconToRender}
focusTriggerAfterClose={focusTriggerAfterClose}
transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)}
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)}
/>
<NoFormStyle status override>
<Dialog
{...restProps}
getContainer={
getContainer === undefined ? (getContextPopupContainer as getContainerFunc) : getContainer
}
prefixCls={prefixCls}
wrapClassName={wrapClassNameExtended}
footer={footer === undefined ? defaultFooter : footer}
visible={visible}
mousePosition={mousePosition}
onClose={handleCancel}
closeIcon={closeIconToRender}
focusTriggerAfterClose={focusTriggerAfterClose}
transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)}
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)}
/>
</NoFormStyle>
);
};

View File

@ -1,5 +1,6 @@
import '../../style/index.less';
import './index.less';
// deps-lint-skip: form
// style dependencies
import '../../button/style';

View File

@ -1,24 +1,24 @@
// TODO: 4.0 - codemod should help to change `filterOption` to support node props.
import * as React from 'react';
import omit from 'rc-util/lib/omit';
import classNames from 'classnames';
import type { SelectProps as RcSelectProps } from 'rc-select';
import RcSelect, { Option, OptGroup, BaseSelectRef } from 'rc-select';
import type { BaseOptionType, DefaultOptionType } from 'rc-select/lib/Select';
import RcSelect, { BaseSelectRef, OptGroup, Option } from 'rc-select';
import { OptionProps } from 'rc-select/lib/Option';
import type { BaseOptionType, DefaultOptionType } from 'rc-select/lib/Select';
import omit from 'rc-util/lib/omit';
import * as React from 'react';
import { useContext } from 'react';
import { ConfigContext } from '../config-provider';
import getIcons from './utils/iconUtil';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import DisabledContext from '../config-provider/DisabledContext';
import { FormItemInputContext } from '../form/context';
import type { SelectCommonPlacement } from '../_util/motion';
import { getTransitionDirection, getTransitionName } from '../_util/motion';
import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import type { SelectCommonPlacement } from '../_util/motion';
import { getTransitionName, getTransitionDirection } from '../_util/motion';
import defaultRenderEmpty from '../config-provider/defaultRenderEmpty';
import getIcons from './utils/iconUtil';
type RawValue = string | number;